Pairs のAPI 開発における OpenAPI 運用の改善と振り返り
これは Eureka Advent Calendar 2023 の 14日目の記事です。
こんにちは。Pairs の Back-end Engineer の emahiro です。
2023年は家族が増え、人生で初めて育休を取得、その後業務に復帰するなど、私生活でもキャリアでも変化の大きな1年になりました。
本エントリでは、そんな自分が育休から復帰後に行っていた、Pairs の API 開発における OpenAPI ドキュメントの運用の改善作業とその振り返りについて記載します。
OpenAPI とは?
詳細な説明は wikipedia さんと公式のドキュメントに譲りますが、ざっくりいうとAPIの定義を記述し、そこからコードの生成等が可能なフォーマットのことです。JSON でも Yaml でも記述が可能です。
The OpenAPI Specification, previously known as the Swagger Specification, is a specification for a machine-readable interface definition language for describing, producing, consuming and visualizing web services.
背景
この改善活動を行おうと考えるに至った背景としては以下の 3点の課題があります。
- 従来の Pairs の API 開発における OpenAPI ドキュメントの運用にはその仕組み上どうしても定義と実装がずれてしまう問題があったこと。
- この課題が残されていたにも関わらず、現在のAPI 開発のワークフローとして「OpenAPI ドキュメントを残そう」という方針が自分の育休中に決まっていたこと。
- SSOT でなかったこと。定義をドキュメントにしても残す先が wiki だったり、場合によっては slack の投稿にあったりとバラバラな状態でした。
(これは自分も関与していたので割れ窓を積極的に作っていたとして今では反省しています…)
Pairs の API 開発における従来のOpenAPI ドキュメント運用と課題
課題についてもう少し詳しく説明します。
実は2020年頃から Pairs の一部の API の開発において API 定義のドキュメンテーションのために OpenAPI ドキュメントが導入されておりました。
その際利用していたツール郡は以下です。
- Schema: JSON Schema
- ドキュメンテーションツール: redocly
- Editor: Stoplight Studio
このときは API の定義の担当者(Pairs では主に Back-end エンジニアが担当する事が多いです)が Stoplight Studio で JSON Schema を記述し、ドキュメントをビルドする、というシンプルなものでした。
ただし、この従来の運用では OpenAPI ドキュメントそのものをドキュメンテーションのソースとしてどうメンテナンスしていくか?、API 開発のワークフローとしてどう組み込むか?という観点が抜けており、定義を記載しても時間が経つと定義と実装にズレが生じてしまうこと、そしてそれを防ぐ仕組みがなかったことで、せっかく生成したドキュメントそのものの信頼性が低下してしまう可能性 (*1) がありました。
※1. 実態としてそこまでズレが大きかったわけではありませんが、すでに存在しない API が残っていたり、また足りない箇所もありました。このへんは導入当時は OpenAPI ドキュメントへの定義が任意(*2)だったこともあり、定義としては SSOT になっておりませんでした。
※2. 担当するプロジェクトごとに OpenAPI ドキュメントを採用するのか、その他の場所に残すのか特に決まりはなかったという意味です。
モチベーション
結論、課題が残されたままルールだけ決まっても、いい感じにワークする未来が見えてこなかったので、チームとして OpenAPI ドキュメントを中心に据えるのであれば、元々あった課題を解決して最低限 API の開発におけるワークフローとして機能させないと正直アウトカムは小さく、負担が増えてペインだけが溜まりそう…という考えに至ったのでこの改善作業を行おうと思いました。
改善作業
背景に記載した課題を解決すればある程度仕組みとしてはワークしそうなイメージは元々持っていたので、チームで書くための仕組みを導入することにしました。
具体的に行った作業は以下です。
- Linter の導入
- OpenAPI の定義で検証を行う。
- OpenAPI の定義から一部のコード(主にレスポンス)のコードを自動生成する。
- OpenAPI を管理してるリポジトリを Pairs の本体のリポジトリに依存させ、2 の検証を CI に任せる。
その他
- SSOT にするために Go のコードから JSON Schema を生成する。
- 定義と実装のズレの部分の修正
- 書ける人を増やす
ざっくり各作業について説明します。
Linter の導入
OpenAPI ドキュメントの記述方法そのものに書き手によるブレが見られてたのである程度定義の書き方のフォーマットを統一するために Linter として spectral を導入しました。spectral は Stoplight を作っているところが提供してる OpenAPI 3.1 向けの Linter ツールです。
この導入のタイミングで v3.0.3 が指定されていたバージョンも v3.1 にアップグレードしました。
Linter の導入に際して追加で行ったことは以下です。
- CI で Linter をかけて warn 以上が出ていたら CI を落とす。
- 既存の定義上どうしても warn 以上の警告が出てきてしまう部分があったのでカスタム定義で severity を変更する。
- Stoplight Studio に限られていた Editor を開発者が好みの Editor を使ってもらえるように Linter の Editor への導入方法を整備する。
OpenAPI の定義で実装の検証を行う
kin-openapi の validator を使っての Pairs 本体に定義された Request/Response を CI 上で検証し、Pairs 側での実装が定義とズレていた場合、CI が警告して落ちる仕組みを導入しました。
サンプルとしてResponseの検証処理は kin-openapi の上記の filter pacakge を利用することで実現できます。
Pairs の API 開発において OpenAPI の定義と API の実装のずれが発生する課題はこの仕組みでほぼ解決しました。少なくともこの検証の仕組みが導入以降に実装された新規の API においては定義と実装が乖離することはほぼなくなっています。(*)
※ 一部古い Endpoint においては、検証が通らず merge が出来ない(kin-openapi の validator の検証を merge の required にしている)という問題があり、そういった一部の API に置いてはこの検証を skip しています。
OpenAPI の定義から一部のコードを自動生成する
上記の OpenAPI の定義から実際の Pairs の API の Request と Response の検証をすることで、OpenAPI ドキュメントの運用で担保したいことは概ね達成されておりますが、OpenAPI の定義から Go のコードを自動で生成する運用を試験的に取り入れています。
Go のコード生成には ogen-go/ogen を利用しています。
Go のコード生成には openapi-generator や oapi-codegen など他の自動生成のライブラリがありますが、今回 Pairs で利用したかった、リクエストとレスポンスだけ先んじて自動生成したいというユースケースにマッチしていたのが ogen だったのでこちらを利用しました。
現状 API クライアントや API サーバーまで含むと、現行の実装とコンフリクトする箇所があるので生成されたコードの一部のみを利用している、というステータスにはなりますが、これだけでも十分実用に堪えるかなとは思います(一部商用環境でも利用しています)
リクエストとレスポンスのコードのみ生成したい場合は以下のような option を付けてコードを生成するサンプルは以下のようになります。
go run github.com/ogen-go/ogen/cmd/ogen@latest -no-client -no-server -target $PathToDocDir
ogen は OpenAPI から Go のコードを生成するライブラリとして候補に上がっていた中ではコミットが盛んで現在進行系で開発が盛んなのでそういう意味では安心感はありつつも、枯れていないというデメリットはあります。
また、自動生成されたコードを使う、ということ自体は実験的な取り組みではあるものの、これは今まで Pairs ではリクエストとレスポンスを全部実装時に自前で定義していた手間が省けること、また OpenAPI の定義からコードを生成してるので、上記の検証は必ずパスする前提で実装を進めることができることなどのメリットもあるので、まずは Back-end Team のメンバーに使ってもらうところから始めています。
使っていく中で課題や改善点は出てくると思うのでブラッシュアップしていきたいなと思います。
OpenAPI を管理するリポジトリを Pairs の本体のリポジトリに依存させる
OpenAPI の定義を使っての実装の検証、及び OpenAPI の定義から生成したコードを実装で利用する、という仕組みを作ること、Pairs の API をサーブしてる Repository を OpenAPI の定義をサーブしてる Repository に依存させています。
依存の方法はGo のエコシステムに則り、OpenAPI の Repository に go.mod を配置して module 化することで go get github.com/eure/$pathToOpenAPIRepo@latest (or commitHash)
で依存を取得、管理をしております。
被依存側の OpenAPI のバージョン管理はタグを切る運用ではなく、コミットハッシュ指定で PullRequest のコミットに含めてもらっています。
最終的に現行の API の開発のプロセスとしては OpenAPI を依存させた後は以下のようになりました。
- OpenAPI で API の定義を記述、自動生成を行い PR を作成。
- Pairs のリポジトリで go get OpenAPI@commitHash (*)
- API の実装(自動生成されたコードを使うかは自由)
- OpenAPI 側で PR を merge
- API の実装の PR で go get OpenAPI@latest
※ go.work に local の OpenAPI の path を定義して push なしで OpenAPI の変更を API のリポジトリ側に取り込み local でリクエストとレスポンスの検証をすることも可能にしています。
go work init . $PathToOpenAPIRepo
このプロセスだと OpenAPI 側の定義を変更してバージョンが新しくなり、かつその変更で下位互換が無くなるなどの破壊的な変更があった場合、CI が落ちたり、ビルドが通らないなど、開発者の手元で定義と実装のずれに気づけるようになっています。
これはAPI の実装時には必ず OpenAPI の定義の追加・更新とセットで進めなければならない制約をチームに課したことになりますが、ドキュメントをかっちり運用していくためには、多少の手間を取ることも必要になる、ということをチームでコンセンサスを取った上で導入しました。
その他
SSOT にするために Go のコードから JSON Schema を生成する
SSOT にするために既存の Pairs でサーブされている Endpoint と各インターフェースを一度 OpenAPI の定義側に記載する必要があったので、Go のコードから JSON Schema を生成して、それを SSOT としました。
この生成方法は同僚の daisuzu san がブログに残しています。
ここで生成された JSON Schema には Description 等詳細情報が欠落している状態ですが、Redocly でビルドしたドキュメントで少なくとも API のインターフェースだけは確認できるようになっているので、歯抜け情報は開発者が気づいたときに埋めていこう、という運用にしています。
実装と定義の修正
これはそのままですね。改善タイミングで I/F レベルでズレている部分については定義を実装に合わせる形で修正しました。
書ける人を増やす
Back-end Team 以外でも OpenAPI ドキュメントを書ける人を増やす活動も少しずつ進めています。
API 一つ作るにも Back-end Engineer では、クライアントの都合等を汲み取りきれないまま API の定義をすることがあるので、クライアントチームの方でも興味がある人向けに少しずつ横展開して、API 開発に関わる人みんなで定義を育てていけるようにしていきたいなと思っています。
今後の改善にむけて
API の定義からドキュメントを生成するといったことは、いまどきの API 開発では一般的なプラクティスとして行われていることかと思いますが、その運用において、定義と実装が100%整合性が取れていることを担保することには労力がかかります。
導入したメンバーにいわゆる「警察業」的な振る舞いをしてもらい、力技で定義と実装を合わせるような運用をせざるを得ないことも少なくないとも思います。こういった API 定義のドキュメント化は実装と定義のずれが無いことが要件として求められるので、定義と実装にずれがある状態が続くと、割れ窓理論でドキュメントを残していくことそのものの運用がなくなってしまう事もありえます。
仕組みの導入自体はある程度熱意のあるメンバーの「えいや」で導入し、使いながら改善していくこと自体は良いことですが、その「仕組みを守らせる仕組み」をワークフロー組み込まずに運用を回し続けるには限界があります。
今回こういった課題を克服するために、Eureka の Back-end Team では最低限の仕組み化と開発のワークフローを定義して、この課題の改善に取り組みました。
ただ、まだまだ良い運用のために改善していかないといけないことは多くあります。
具体的には上記の改善作業の中で説明したことの他にも開発者にとってJSON 書くのがしんどい問題と、redocly で生成したドキュメントが重たい問題があります。
JSON Schema 書くのがつらい (Alt JSON なツールの導入検討)
JSON Schema を補助輪なしで記述するには一定慣れが必要だと考えていますし、そもそも JSON は書いてて楽しいフォーマットではありません。Yaml を使ってもいいですが、別に JSON と比べてどうかという話になると Back-end Team としてはどっちもどっちで無理に Yaml にするほどでもない、という感想があったりするので、元々使われていた JSON Schema を今はそのまま使っています。
(Yaml vs JSON といった議論は生産的ではないのでここではしません。)
代替手段としては Cue -> JSON/Yaml を生成したり、TypeScript -> JSON Schema あたりが候補に上がるかなと考えていたりします。このあたりはまたタイミングを見つけて PoC してみたいなと思います。
生成したドキュメントが重たすぎる
OpenAPI の定義から redocly でドキュメントを生成し、生成された HTML を GitHub Pages でサーブしてるのですが、この HTML がすべての定義を1つにまとめた JSON (数万行あります)から作られているのもあり、このファイルだけで数十MBのサイズがあって、ブラウザでドキュメントを見るときに、とにかくめちゃくちゃ重たいです。HTML の DL だけで数秒かかってしまうので現状取れる手段が HTML のサイズを小さくする(= JSON をいい感じの区切りでいい感じに分けて、分割した JSON ごとに HTML を生成する)くらいしかできることが実はありません…。
こちらもディレクトリの構成と redocly で HTML を生成するときのコマンドを工夫することでリソース(やドメイン)単位で JSON を分けていたりする箇所もありますが、そうなると SSOT ではなくなってしまいます。
SSOT であることを担保するために、分割したドキュメントごとへのリンク集等を作らないと、この分割の方法はワークしなさそうだなと思っているところですが、ここに関してはまだ確たる方針は決まっていません。
まとめ
ざっくり、Pairs で OpenAPI ドキュメントをワークフローに取り入れているのか、そのためにどういった改善作業を行ったのかを紹介させてもらいました。
まだまだチームとして作った仕組みを回し始めた段階ではあるので、課題となる点はまた出てくると思います。残すところは残し、捨てるところは捨てつつ、自分たちの API 開発の仕組みを少しずつ改善していこうと思います。
それではみなさん、良いお年を〜 👋