Carpe Diem

備忘録

OpenAPI Specベースのモノリポのドキュメントを1つにまとめる

背景

実装とAPIドキュメントはしばしば乖離して負債になりがちです。なので

  • 実装コードからAPIドキュメントを生成する
  • OpenAPI SpecのようにAPIスキーマから実装コードを生成する(スキーマ駆動)

といった方針をとることが多いです。

FastAPIは前者のパターンで、実装から自動でOpenAPI Specのドキュメントを生成できます。 後者は以前紹介したこともあります。

OpenAPI で REST API のスキーマ作成 - Carpe Diem

今回は多数のFastAPIベースのマイクロサービスをまとめたモノリポ構成において、ドキュメントを1つにまとめたいと思います。

環境

  • FastAPI v0.76.0
  • uvicorn v0.17.6
  • swagger-ui v4.11.0

モノリポディレクトリ構成

以下のようなディレクトリ構成だとします。service2, service3の中身はservice1と同様とします。

monorepo-documents
.
├── LICENSE
├── README.md
└── python
    └── services
        ├── service1
        │   ├── README.rst
        │   ├── poetry.lock
        │   ├── pyproject.toml
        │   ├── service1
        │   │   ├── __init__.py
        │   │   ├── config.py
        │   │   └── main.py
        │   └── tests
        │       ├── __init__.py
        │       └── test_service1.py
        ├── service2
        └── service3

方法

今回の要件は以下とします。

  • 1つ1つのサービスで見ることができるドキュメントを1つのSwaggerUIで管理
    • マイクロサービス毎に/docsでドキュメントは見ることができるが、LBへのpathの追加といった運用は避ける
  • Dockerで管理

完成イメージとしては

このようにプルダウンで切り替えられるようにします。

方針としては以下でやります。

  • 1つ1つのサービスでopenapi.jsonを吐き出す
  • 1つのSwaggerUIに↑を保持させ、それぞれ切り替えて参照できるようにする

1つ1つのサービスでopenapi.jsonを吐き出す

docs.pyの用意

FastAPIはドキュメントを一部カスタマイズすることができます。

それを用いながらpythonスクリプトを実行した時にjsonファイルを生成するよう、以下のdocs.pyを配置します。

import json
from fastapi.openapi.utils import get_openapi
from main import app
from config import settings

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title=settings.service_name,
        version=settings.version,
        routes=app.routes,
    )
    app.openapi_schema = openapi_schema
    return app.openapi_schema

if __name__ == "__main__":
    with open(settings.service_name+"-docs.json", "w") as fd:
        print(json.dumps(custom_openapi()), file=fd)

シェルスクリプトMakefileの用意

リポジトリルートから一括で生成できるようにします。

シェルスクリプト

サービス毎のディレクトリ構成に規則性を持たせることでスクリプトで扱いやすくします。

#!/bin/bash
set -eux

ROOT_DIR=`echo ${PWD}`
DOCS_DIR=$ROOT_DIR/docs

DIR=`echo $1 | awk -F 'docs.py' '{print $1}'`
TARGET=`echo $1 | awk -F '/' '{print $(NF-2) "-docs.json"}'`
cd $DIR
poetry install
poetry run python docs.py
mv $TARGET $DOCS_DIR

Makefile

Makefileで管理することで

DOCS_LIST := $(shell find . -name "docs.py")
DOCS_TARGETS := $(addsuffix .docs,$(DOCS_LIST))

docs: $(DOCS_TARGETS)
%.docs:
    ./scripts/docs.sh $@

以下のように-jオプションを使って並列実行することが可能です。

$ make -j 3 docs

1つのSwaggerUIに↑を保持させ、それぞれ切り替えて参照できるようにする

SwaggerUIはurlsを設定することで複数のopenapi.jsonを管理できます。

{
    "urls": [
        {
            "name": "Master API",
            "url": "specs/master.yaml"
        },
        {
            "name": "Bot API",
            "url": "specs/bot.yaml"
        }
    ]
}

ref: swagger-ui/configuration.md at master · swagger-api/swagger-ui · GitHub

これを設定するとSwaggerで通常URLを指定する箇所がプルダウンに変換されます。

jqを使ってconfig jsonの生成

これを利用して先程生成した個別のxxx-docs.jsonを参照できるようなconfig jsonをjqを使って生成します。

#!/bin/bash
set -eux

cd docs
for file in $( ls *-docs.json ); do   
  URL="./api/$file"
  jq -n --arg file ${file} --arg url ${URL} '{"urls":[{"url": $url, "name": $file}]}' > tmp-sc-$file.json
done
jq -s '.[0].urls=([.[].urls]|add)|.[0]' tmp-sc-* > swagger-config.json
rm tmp-sc-*

実行してみると以下のファイルが生成できます。

{
  "urls": [
    {
      "url": "./api/service1-docs.json",
      "name": "service1-docs.json"
    },
    {
      "url": "./api/service2-docs.json",
      "name": "service2-docs.json"
    },
    {
      "url": "./api/service3-docs.json",
      "name": "service3-docs.json"
    }
  ]
}

Dockerfileの用意

swagger-uiのdocker imageにjsonファイルだけ転送します。

FROM swaggerapi/swagger-ui

COPY *.json /usr/share/nginx/html/api/

動作確認

docker imageが完成したら起動してみます。

すると要件を満たしたSwaggerUIが起動できました。

その他

サンプルコード

今回のサンプルコードはこちら

github.com

CONFIG_URLyamlファイルは使えないのか

どうやら現状はjsonファイルしかサポートしていないようです。

Just tested, seems like documentation is really outdated. swagger-config.yaml is not supported for now, only swagger-config.json

urls option from custom swagger-config.yaml ignored. · Issue #6019 · swagger-api/swagger-ui · GitHub

まとめ

やや泥臭い感じになりましたが、一度仕組み化すれば特にメンテせずとも自動的にドキュメントを1つに集約して管理できるようになりました。

参考