Open10

スキーマ間の互換性について考察、思考垂れ流し

ハトすけハトすけ

インフラ間のスキーマの互換性の不一致によるバグに悩んでいる。
なぜそのようなバグがおきるかというと、ts-restを使って、バックエンド側とフロントエンド側のスキーマを型安全に一致させているが、それがリリースのタイミングのずれで、フロントエンドの依存しているスキーマがバックエンドに存在しないという事になってしまう。それを防ぐにはリリースの順序をコントロールしないといけなく、大変。ここらへんをもっとうまくできる仕組みがほしい。

ここらへんを、前回スクラップで垂れ流していた。
https://zenn.dev/dove/scraps/f3a6dba3e41dcc

ハトすけハトすけ

今、アイディアとして持っているのが2つ。後者は前回いっていたアイディアに近い。

  • フロントエンドのスキーマとバックエンドのスキーマを独立させ、リリース時にスキーマ同士の互換性テストを行うこと
  • APIのスキーマ自体はnpmライブラリとして保持し、バージョン管理を行う。メジャーバージョンが一致していれば互換性が壊れないように作っておき、マイナーバージョンは自動リリース、メジャーバージョンは手動リリースを行う
ハトすけハトすけ

後者はコメントによるGitHub Actionsの条件分岐とかでつくれそう。

基本的に
フロントエンドのスキーマメジャーバージョン == バックネットのスキーマメジャーバージョン
フロントエンドのスキーママイナーバージョン <= バックエンドのスキーママイナーバージョン
という条件を満たしていればバグは発生しない。フロントエンドはロールアップで、依存関係のバージョンアップサイクルの1つとしてバージョンあげしていくフローかな。

ハトすけハトすけ

一見良さそうなこの方法も 依存する値の削除を具体的に考えたときややこしくなる

フロントエンドが依存しているある値Aがあったとする。それをバックエンド側で削除したい。
基本的には、バックエンド側がある値Aをdeprecatedとして指定する。フロントエンド側は、IDEのサポートを借りつつ、それがdeprecatedであることを認識し、なるはやで依存を解消しないといけない。
バックエンド側はdeprectedの目印をつけたなら、次のメジャーバージョン以降は削除してもよい。

この運用方法は、安全だが、変化のスピードが遅くなる。また、セマンティックバージョニングだから削除してもよい、という正義がありつつも、実際はフロントエンドから削除されなければエラーが起きてしまうので、フロントエンドが依存をなくしたことを、コード外のコミュニケーションで確認しないといけない。

この運用方法は、変化の早い社内向けではなく、どちらかというとセマンティックバージョニングというルールに基づいたOSSコミュニティ向けなのではと思う。

ハトすけハトすけ

前者の方法だとこのデメリットを解消できる。

フロントエンドが要求するスキーマと、バックエンドが発信するスキーマを別々で保持しておく。お互い完全オリジナルでもよいし、スキーマ自体のバージョンの違いだけでも良い。とりあえずインフラごとにスキーマを記録する。

そして、バックエンドのリリース時かCI時にフロントエンドとのスキーマの互換性テストを明示的に行う。
もしくは、フロントエンドのリリース時かCI時にバックエンドとのスキーマの互換性テストを明示的に行う

そのテストに合格すれば、自動的にリリースできる

ハトすけハトすけ

前者と後者の違いは、単にバージョン番号で互換性を信頼するか、スキーマ自体で互換性を信頼するか。

値の削除のとき、違いが明確になる。

前者の場合:

フロントエンドである値Aに依存していたとする。バックエンドでその値Aを削除したい。バックエンドで削除するようなスキーマを書くと、スキーマ互換テストで失敗してしまう。なぜならフロントエンドのスキーマがそこに依存しているから。そこで、フロントエンドチームに連絡をいれフロントエンドのスキーマをある値Aが依存しないように更新してもらう。その分フロントエンドのコードベースの変更は発生する。フロントエンドのスキーマ変更が確認されたのち、バックエンドは安全に値Aを削除することができる。なぜなら、スキーマ互換テストが成功するからだ。

後者の場合:

フロントエンドである値Aに依存していたとする。バックエンドでその値Aを削除したい。しかし勝手に削除するとフロントエンドに影響がでそうなため、一旦deprecatedして触らないで置く。フロントエンドチームに連絡し、次のメジャーバージョンアップでこの値を削除するからそれまでに依存からなくすように伝える。フロントエンドチームから値Aへの依存をなくしたことを聞いて、メジャーバージョンアップを行う。

ハトすけハトすけ

GraphQLでも同様に互換性のバグを解消できそう。

なぜならGraphQLはデータの所有権がフロントエンドが強いから。値がいらなくなると、フロントエンド側が逆deprecated指示を出せる。依存側がdeprecatedするのだから、そもそも互換性バグが生じにくくなる。

ハトすけハトすけ

まぁあとはRailsみたいに、モノリスにすれば互換性の問題も少なくなる。

ハトすけハトすけ

フューチャー技術ブログにのってたgrafanaのthemeというプロジェクト、かなり面白そうだな。

https://github.com/grafana/thema

互換性があるスキーマはシーケンスに、ないものはスキーマとして保持し、それを前後比較。バージョン感の差分をlensに記録するのか。

また検証プロセスも
流れているデータを最初からスキーマ1と定義づけず、スキーマ1のバリデーションにとおったからスキーマ1としてみる、みたいな流れなのもいいな。
APIサーバーから流れるデータはかならずしもスキーマ1とはかぎらずスキーマ1.1とか0.9かもしれない。スキーマとデータを分離するの、良いヒントになりそう。