はじめに
2021年12月18日ドワンゴのアドベントカレンダーになります。 ここで紹介する内容は@Himenonがニコニコ生放送のWEBフロントエンド開発を安全かつ長期的に運用するために投入した技術を紹介します。
まず前提として社内のGitHubはEnterprise版を採用し、Internetから隔離された環境にありいくつか機能がOFFにされた状態で利用されています。 また、CI系のツールもDrone 0.8.0を使用しているため、各種エコシステムは基本的には利用できません。 (一応釘を刺しておきますが、これらの機能がONでもサードパーティ製のエコシステムが信頼できるかどうかは別で考える必要があります)
導入した結果継続的に運用されているもの
1. ファイルサイズの監視
おそらく最も利用されているツールはファイルサイズの変化の監視です。 webpack-bundle-analyzerを定期的に見る、と言ってもやはりわざわざHTMLのページを開いて確認する手間は人間しないもので、かつ、変化がすぐに分かるという状態は作れませんでした。 したがって、我々開発者が本当に欲しかったものは自分の作成したPull Requestがどの程度ファイルサイズに影響をもたらすものかMergeする前に知ることでした。
機能紹介
ライブラリはすでに公開しており、@himenon/performance-reportを利用しています。 これはGitHubのリポジトリをデータストアとして利用する仕組みになっており、PrivateなGitHub Enterpriseでも利用できるように設計されています。 CI上で稼働し、コミットハッシュをキーとしてファイルサイズを保存し、Pull Requestのコミットハッシュから一番近いコミットハッシュのファイルサイズと比較し、Pull Request上にレポートを投稿します。マージをトリガーとして計測結果をデータストアとしたリポジトリにコミットしています。
事例
例えばwebpack4からwebpack5にアップグレードしたPRの変化は次のように計測されました。 webpack5の更新に伴ってファイルサイズは減ると想像はできますが、実際にはwebpack5に上げるに伴い周辺ライブラリのアップデートも必要なため一概に減るとは言えないことがわかります。
またファイルサイズの変化がPull Request単位で確認することができるため、ファイルサイズの削減がわかりやすい形で表示されます。 例えばlodashの全体をimportしていた箇所を修正した例は以下のようになります。
このファイルサイズのPull Request単位の可視化によってもたらされたよい影響は次のものが挙げられます。
- ファイルサイズの増減表がPull Request単位で可視化される
- Pull Request単位でファイルサイズの変化が見れるため、開発者が言わずともレビュー対象として自然に見るようになった。
- ファイルサイズが大きくなるようなPull Requestが発生した場合、「なぜ増加したか?」を調査してからマージするようになった。
- これはRenovateが自動で出すPull Requestも同様に適用されます。
課題と反省点
計測面
- 計測対象はビルドした成果物を対象とするため、ビルド時間の長いプロジェクトはPull Request単位でビルドするとCIの実行時間が長くなる。
- 回避策(1) マージをトリガーとしてビルドと計測を実施し、マージされたPull Requestにレポートを返すようにできます。
- 回避策(2) 定常時はPull Requestでビルドと計測を実施しないが、特定のGitHubのラベルを見てビルドと計測を有効にするようにする。
実装面
- ブランチを分けたときに計測がうまくできない
- コミットハッシュでソートしている(つもり)だが、計測結果がおかしいときかがある
- リポジトリ名失敗してる(performance-reportは守備範囲がでかい)
- JSON形式で保存しているが、GrafanaやRedashなど可視化ツールを連携するところまでできていない
- 運用上なくても困らない機能だが、今回のような記事を書くときにデータ数が多いので変化が激しい場所を見つけるのが大変だと気づいた
2. npmパッケージの逆依存関係の可視化
機能紹介
GitHubのInsights > Dependency Graph > Dependents
をnpmパッケージ向けに機能強化した静的なWEBダッシュボードです。
社内のGitHub Enterpriseを定期的にスクレイピングして社内向けにリリースしたnpm packageがどのリポジトリでどのバージョンが利用されているか確認するために利用することができます。
OSSとして公開されています。
事例
影響範囲調査
ライブラリ変更時の影響範囲調査や、変更作業のクリティカルパスを作成するために利用しています。Cron Jobで滴定に社内のリポジトリをスクレイピングしてデータを更新しています。
Renovateの更新順序整理
Renovateによるライブラリの自動更新を最適化する際、複数更新が必要な箇所(菱形依存/継承のような形)を見つけて依存関係を単方向にしたり、モノレポにしたりする差異の分析手段として利用しています。
展望
ツール自体の課題は上げたらきりがありませんが、事例で上げたような課題はチームの努力によってどんどん小さくなっています。 それによりツールはその役目を終える未来が見えています。しかしながら、npmライブラリ自体は存在し続けるためその依存関係は健在です。 モノレポ化したから解決、ではなくモノレポ内でも依存関係を可視化する必要があり、sort-dependencyの機能を取り込み 巨大なモノレポであってもその依存関係がすぐに把握できるようなツールに変貌させる必要があると考えています。
3. OpenAPI SchemaによるAPI Clientの運用
小話
従来からあるWEBフロントエンドで利用するAPI Clientはバックエンドの仕様書をもとにコツコツとTypeScriptを記述して実装に落とし込む方式でした。 さまざまなマイクロサービスが存在するため仕様書もバラバラで、実装者もバラバラなためAPI Clientの実装も厳格に統一されているわけでは有りませんでした。
そのためAPI Clientの開発が苦であり、作ったら見たくないものの一つでした(自分は)。 2017年11月26日に開催されたNode学園祭で@pika_shiさんによるJSON Schema Centralized Designを聞いたときからずーーーーっとコレを導入できないか考えていて、以下のようなIssueを作成しました。(このときリポジトリも作った)
その後、色々と既存のCodeGeneratorを使ってみるのですがニコニコ生放送で使われているAPIの構造や、TypeScriptの型の厳格さ、ポータビリティがどれも噛み合わず、Issueは作ったもの大手を振って「導入しよう!」と言えるものじゃなかったので悩んでいたのですが、例によってその月の末にJSConfJPが有りまして、@t_wadaさんのJAVASCRIPT AST プログラミング: 入門とその1歩先へを拝聴したんですね。コレなら自分でも作れるんじゃないかと思って、2週間くらいかけてやっとJSONSchemaをTypeScriptの型定義に書き起こすことに成功したので作ることにしました。コツコツと1ヶ月くらいかけて作りつつ、チーム内で利用しているAPIのOpenAPI Schemaを全部書き出してみてそれをテストデータに耐えられるものに仕上げていきました。
やはり実際に利用されているSchemaをテストデータとしたのは正解で、多くの問題を発見するとともにどうしたらOpenAPI Schema自体のメンテナスコストを減らすことができるのか、も同時に考えられました。問題となったのが以下の点、
- OpenAPI Schemaを書くときにSchemaのファイルが大きくなりすぎる
- 外部参照(
$ref
)の自由度が高すぎる - 多くのCodeGeneratorが出力するコードはファイル分割がされているためエディタのコードジャンプが遅い(数千ファイルを超える巨大なプロジェクトを開くとジャンプも秒単位かかる)
がありました。外部参照の自由度は厄介で、ディレクトリ構造が荒れる未来が容易に見えます。
したがって、Code Generatorのデザインとしてこれを逆手に取り、階層化されたディレクトリをそのまま型定義に反映させることで、型定義の階層構造をきれいにすること = Schemaのディレクトリ構造をきれいにすること
と等価な状態にすることで荒れることを防いでいます。他の設計思想はリポジトリのREADMEを御覧ください。
導入事例
さて、前置きが長くなりましたがどうなったか。いくつかのプロジェクトは自分が責任を持ってすべてOpenAPI SchemaとCode Generatorが生成したAPI Client利用したコードベースに書き換えました。 本番での稼働実績を時間が証明し、徐々に他のプロジェクトでも以降が進められています。
運用を開始して1年近くなりますが、大きな問題は発生することなく、いくつかの発見を得ることになりました。
発見1: API Clientの責務
API Clientはバックエンドのサービスとクライアントの境界だけが責務であることです。 何が言いたいかと言うとAPI Clientの型定義内にアプリケーションレベルのドメイン知識が入ってはいけないということです。 必ず変換層が存在する必要がありますが、実際には変換層がないケースのほうが多くありました。 なぜかと言うと「たまたまバックエンドのレスポンスがクライアントアプリケーションで必要な型定義をしていた」からのケースだったからです。 型定義がアプリケーションとAPI Clientで同じものが使われている場合、その箇所は修正が大変になることが予測できることがわかりました。
# 変更がバックエンドに引きずられる可能性が十分に高く、型定義の依存の結合度が高い
Client Application <--> API Client <--> Backend Service
# 変換層に変更が吸収される・型定義が分離できる
Client Application <--> 変換層 <--> API Client <--> Backend Service
API Clientとそれに付随する型定義を自動生成することにより、人為的な介入ができなくなったため、責務が顕になった事例です。
発見2: コア部分はDI(Dependency Injection)で実装されるためライブラリの影響を受けない
これはそもそもの設計の性能ですが、axios
やnode-fetch
など特定のライブラリにAPIのコア実装部分が依存していない事によりあらゆる事象に柔軟に対応できました。
特にaxios/axios#3930で言及されようにNode v14に上げる際にaxiosを利用していると一部の機能が利用できない不具合を持つようなケースです。従来のapi-clientの実装のままだとaxiosに依存しているapi-clientが存在していましたが、今回作成したCodeGeneratorは任意のfetch系クライアントをimpl実装するだけで導入できるため、利用するライブラリを交換するだけで対応できました。
課題と展望
@himenon/openapi-typescript-code-generatorはOSSとして公開されており、すでに何名かのContributionを頂いております。本当にありがとうございます。
現在もさまざまなOpenAPI SchemaをCodeGeneratorに通して、ビルド可能な型定義が生成されるようにコツコツとパッチを当てている状態です。 全体の最適化はいずれしたいと考えていますが、規模が大きいのでもう少し軽量な状態を目指せないか試行錯誤中です。 TwitterやGitHubは気がついたらリプライやレビューは続けるつもりです。
もっと読みたい人向け
- リポジトリの運用は別記事(OpenAPI(Swagger)を利用してTypeScriptのAPI Clientを自動生成する設計と実装)で詳細に書いているので御覧ください。
- @himenon/openapi-typescript-code-generator
最後に
ここでは1年以上、今もなお運用され続けているツールを紹介しました。 紹介できなかったものもあれば、まだ一年経っていないツールも眠っています(失敗作も!)。
またどこかのタイミングで紹介しようと思います。最後まで読んでいただきありがとうございました。