現実世界における、スキーマ設計の妥協日本経済新聞社Yuta IdeEncraft #2 サーバーとクライアントを結ぶ技術1
View Slide
自己紹介module.exports = {name: “Yuta Ide”,belong: “日本経済新聞社”,role: [“Client”, “Edge”, “BFF”],lang: [“TypeScript”, “Rust”].push(“Scala”),“schema now I’m using”: [“JSON Schema”, “zod”, “joi”, “GraphQL”, “pbuf”]}2
3
羨ましい!!!4
tl;drスキーマレスに10年以上開発されているものに、スキーマを導入するのは難しすぎる!5
日本経済新聞社の負債返済活動● 3ヶ月開発を止めて非機能要件開発○ Node.jsアップデート作業○ アラート改善活動○ スキーマ改善活動○ パフォーマンスメトリクス計測の仕組み○ 過去ページのアーカイブ化(SSG)○ E2E整備https://speakerdeck.com/sadnessojisan/jian-shi-senaakansi-wu-da-zhi-dakeniookamitutenahttps://www.youtube.com/watch?v=L--AVk29m6w 6
スキーマ改善活動手書きのAPIスキーマ定義、実際の値と異なっているから直しても良かです???わぁ、いいよ!ありがとう!!!7※ここまで和やかな職場ではありません
スキーマ改善活動● バックエンドチームを兼務してSpecやMiddlewareを書いていた● 手書きyaml の Spec から Validator を生成して、それを Django (Python)に組み込む仕組みを作った● そして現実を知るのである、「Specが信用できないのはAPIチームのサボタージュや技術不足ではなく、そもそも・・・」スキーマ駆動開発導入の難しさを知る8
スキーマ駆動開発とは9
ここでの定義は、フロントエンドとバックエンドで、疎通のインターフェースを合意し、そのインターフェースに従う形でお互いが開発すること。10
スキーマの運用を疎かにすると何が起きるのか● サーバーの開発が終わるまでフロントは疎通できない● クライアントが想定していないデータを受け取った場合、クライアント側でランタイムエラーが発生する(かもしれない)● サーバーからどういうデータが返ってくるのか、コンピュータは分からない11API開発フロント開発API改修フロント対応BEFE
反対にスキーマがあると何が嬉しいのか● サーバーサイドの開発を待たずにどういうレスポンスが返るか分かる● サーバーの戻り値の型を得られる● APIクライアントも得られる(作れる)12API開発フロント開発API改修フロント対応schema schemaBEFE
スキーマ通りの値をサーバーが返すことが前提スキーマは実値を表しているとは限らない!!!!13
ドキュメント・バリデータ・型を同期させよ!https://blog.ojisan.io/swagger-validator-ts/14
フロントエンド開発における代表的な技術15
JSON Schema● スキーマ: JSON Schema● 型: typebox, json-schema-to-ts● バリデーション: ajv16
Swagger● スキーマ: Open API Spec● 型: swagger-typescript-api● バリデーション: swagger-model-validator, ajv(OAS is based on jsonschema)17
zod● スキーマ: zod● 型: z.infer● バリデーション: z.parse()18
GraphQL● スキーマ: GraphQL schema● 型: graphql-code-gen● バリデーション: ??? (graphql-codegen-typescript-validation-schema というのが最近あるらしいが筆者は触っていない)19
gRPC● スキーマ: Protocol Buffers● 型: ts-protoc-gen, ts-proto● バリデーション: ???20
バリデーションは本当に必要?21
client APIDataSourceスキーマ駆動開発はここの話22
client APIDataSource23validationvalidationスキーマ駆動開発はここの話
client APIDataSource型や契約が満たされている処理24
client APIDataSource型が付いている通信型が付いてるのに、validation 必要???25
バリデーション系の話をあまり聴かない● ajv はともかく、schema に対するバリデーションの話を聞かない○ ajv は逆で、バリデーションライブラリがあって、要求しているIDLがJSON Schemaという関係● unknown な箇所に validation して型をつけた後のレスポンス作成において、プログラミング言語が型安全を保証しているのであればvalidation は不要では?26
正しい、静的な型があるとは限らない● そもそも静的型付け言語でなければ型安全にならない● 動的型付け言語ではそもそもレスポンスの形に型付かないし、コードからドキュメントやSpecを生成できない(難しい)● 静的な型があったとしても、型をごまかすハッチの存在、型指定をうっかり忘れるなど、多くの言語では型に対してウソをつける相手が信用できないのなら、スキーマ通りの値か検証した方が良い27
コードから生成しないSpecは嘘をつけるPOST /users絶対200手書きSpecWikiスプレッドシートなんでもあり実装28
言語・FW選択で割と勝敗が決まる● コードからドキュメントやSpecを生成するのが苦手な言語やFWというものは存在する● 静的型付け言語で書かれたコードなら、型情報を使ってレスポンスのSpecを生成しやすい● もちろん自分で解析したらどの言語でもできるが、「そこまでできますか?」という問題がある● 例: MVC FW からモデルを介さずにレスポンスを作る29
言語・FWに依存しないスキーマ駆動開発をしたい!30
JSON Schema という妥協31
日本経済新聞社のアーキテクチャCDN OriginAPIGW13年間も積み重なったレガシー&入稿基幹サービス群Frontend Teamの持ち物信用できないデータ32Swaggerとかが生まれる前から稼働しているサービスなのだから仕方がない!
日本経済新聞社のアーキテクチャCDN OriginAPIGW13年間も積み重なったレガシー&入稿基幹サービス群Frontend Teamの持ち物信用できないデータ33Gateway の責務はGateway なので素通しする信用できないデータ
日本経済新聞社のアーキテクチャCDN OriginAPIGW13年間も積み重なったレガシー&入稿基幹サービス群Frontend Teamの持ち物信用できないデータ34信用できないデータどちらかでバリデーションをしたい
日本経済新聞社のアーキテクチャOriginAPIGW信用できないデータ本当はここでバリデーションをして、信用できるデータにしたい35
日本経済新聞社のアーキテクチャOriginAPIGW信用できないデータ本当はここでバリデーションをして、信用できるデータにしたい36一部APIにおいて、バリデーションに使うためのOAS準拠の仕様書が存在せず、サーバー側でバリデータを作れない。(スプレッドシートにある)
日本経済新聞社のアーキテクチャOriginAPIGW信用できないデータ(APIGWから見た)クライアント側でバリデーションする37
日本経済新聞社のアーキテクチャOriginAPIGW信用できないデータ(APIGWから見た)クライアント側でバリデーションする38クライアントサイドには実績ある型定義があるので、それをスキーマとして使うことができる。
つまりクライアントが勝手にスキーマを持つ39
ところでそれはスキーマ駆動開発ですか?● クライアントが独自にスキーマを持ってしまうと、もうそれはスキーマ駆動開発では無い● ただし、クライアントのスキーマをサーバーが参照して開発しているのであればスキーマ駆動開発もできるvalidation & typing のためのスキーマを作るだけ40
クライアントで完結する技術選定● そりゃあGraphQLやらgRPCが理想ですよ・・・● が、それらの導入には APIGW の開発が必要となり、現実的では無い● JS/TS前提のスキーマライブラリを考える○ joi○ yup○ zod○ ajv41
クライアントで完結する技術選定https://blog.ojisan.io/i-use-ajv-instead-of-zod/42
選ばれたのは JSON Schema でした● 2023年現在、一番良いのは zod だと思う● が、これまでの歴史上 joi -> yup -> zod とスキーマライブラリが乗り換えられている● zod の次が出た時、zod ベースのソースはどうなるのか?スキーマはサーバーの根幹であり、そこが流行に振り回されていいのか???● 長生きするIDLを採用したい -> JSON Schema言語や流行に左右されないIDLを使おう43
JSON Schema とは● JSON で定義する、JSONの形● 詳しくは JSONとJSON Schemaを改めて理解する44
ajv とは● Another JSON Validator● JSON Schema で定義されたvalidator を作れるhttps://ajv.js.org/45
fastify との相性が良い● frontendチームの技術スタックはFastify での自作SSR● Fastify は Ajv が標準で組み込まれており、req/res を検証・シリアライズできる● APIを持つ場合のSpec生成にSwagger を使う後JSON Schema からSwaggerを吐き出すプラグインがあるhttps://blog.ojisan.io/swagger-validator-ts/46https://blog.ojisan.io/swagger-validator-ts/
スキーマ導入までの道筋1. スキーマを生成する2. バリデータを生成する3. バリデーションに失敗した時のハンドリングをする47
スキーマ導入までの道筋1. スキーマを生成する2. バリデータを生成する3. バリデーションに失敗した時のハンドリングをする48
クライアントサイドにどうやって Schema を作るか● 手書きされた API Spec から JSON Schema を作る○ 手書きされたSpecが実値と乖離しすぎていて却下● サーバーの実値から JSON Schema を生成する○ 動的なキー(やめろ!)やOptionalがあるので実値からだとスキーマの完成形が分からない● TypeScriptの型から JSON Schema を生成する○ クライアント側はすでに await res.json() as any as Type としていて稼働実績ある型を使っている。それが正しいのかはさておき・・・49
TypeScriptの型から JSON Schema を生成する● quicktypeやtypescript-json-schema● すでにクライアントサイドにある型からJSON Schemaを自動生成する。運用している型なのである程度正しい実績もある。● 実は Optional, Nullable, Undefined の扱いが怪しい・・・https://blog.ojisan.io/typescript-json-schema-ajv/50https://blog.ojisan.io/add-nullable-to-json-schema/
1 API に 610 行のスキーマが生成される。コードからの自動生成でしかスキーマを導入できない!
1 API に 610 行のスキーマが生成される。コードからの自動生成でしかスキーマを導入できない!_人人人人人人人人_> Over Fetching < ̄Y^Y^Y^Y^Y^Y^Y^Y^ ̄
スキーマ導入までの道筋1. スキーマを生成する2. バリデータを生成する3. バリデーションに失敗した時のハンドリングをする53
バリデーションしたあとの値には型がついて欲しいUser型がついて欲しい54
ajv で型を付けるJSON Schemaに対応する型のGenericsを渡す。(出鱈目な方を渡すとコンパイルエラーになる)型がつく55
JSON Schema に対応した型を作る● 今回はTSからJSON Schema を生成したので考えなくてもいいが、そうで無い場合どうすればいいか● TS first な JSON Schema を生成できる IDL を使う○ typebox○ zod● json-schema-to-ts の型 Utilhttps://blog.ojisan.io/ajv-to-type/56
スキーマ導入までの道筋1. スキーマを生成する2. バリデータを生成する3. バリデーションに失敗した時のハンドリングをする57
バリデーションに失敗すること前提で作る● そもそも “single source of truth” が存在していないところにスキーマを導入するので、バリデーションがスキーマ通りに成功する訳が無い● 残念ながら、バリデーションに失敗した時にそこで例外を投げられない● 例:記事表示ページで、おすすめ記事一覧リストのデータがバリデーション違反でした。例外を投げて記事全体を表示されなくなることが許されるのか?58
バリデーションに失敗すること前提で作る● バリデーションに失敗したら型を無理やり付けて素通しさせる● ただしSentryなどに吐き出されるようにしておく59
スキーマ違反を素通しさせて意味があるのか?● 「結局スキーマ違反な値にウソの型を与えてクライアントに返せば、クライアント側でランタイムエラー起きますよね」「わかっとるわ!!!!」● Validation Error を集計することが大切● クライアントが期待するデータに対する Validation Error を集めることで、APIチームやさらにその裏側に「こういうデータを返してください」と言えるAPI側の手書きスキーマを改善するサイクルを作るための第一手60
もしゼロから作り直すならどうするか61
日本経済新聞社のアーキテクチャCDN OriginAPIGW13年間も積み重なったレガシー&入稿基幹サービス群Frontend Teamの持ち物信用できないデータ62信用できないデータどこかでバリデーションをしたいどこか一層に信頼できないデータ層が生まれると、その前段に信頼できないデータは伝播していく。つまりスキーマ通りの値を返す仕組みを作るのであれば、入稿やDBのスキーマから正しいスキーマを伝播していく必要がある
日本経済新聞社のアーキテクチャCDN OriginAPIGW13年間も積み重なったレガシー&入稿基幹サービス群Frontend Teamの持ち物信用できないデータ63信用できないデータどこかでバリデーションをしたい信用できないデータソースがある状況で信用できるデータを返すようにしたいのであれば、結局はどこかの層でバリデーションは必要で、誰かがAPIがエラーを返す覚悟で例外を投げなければいけない
リプレイスする時に守る1つの鉄則● スキーマやドキュメントに違反した値を返してはいけない○ 外部に公開するAPIは必ずSpecを公開する○ Spec違反の値を返さないように検証する● 防御的(!?)・予防的・攻撃的・契約プログラミングの考え方を自チームや自社に当てはめて戦略を考えよう○ 0ベースで開発する場合、違反するものは落として欲しい64https://speakerdeck.com/twada/php-conference-2016
DBや入稿側から正しいスキーマを伝播させる● DBに面する入稿システムやマイクロサービスが、DBのスキーマを元に型安全なAPIを作ると信頼を伝播させられる○ ORM○ Compile-time SQL CheckDBからクライアントまでを型安全に繋ぐ技術を採用する65
例: ORM● Entity とクエリのマッピング● DBからの値が Entity として型がつくようになる● 例) Prisma, TypeORM, Active Record, Django ORM…66
例: sqlx● Compile-time checked queries● ORMではないが、コンパイル時にスキーマを検証し、DTOへのマッピングまでもマクロで可能● コンパイル時にDBに対してクエリが走り、クエリの実行結果とRustの世界で型検査できるhttps://github.com/launchbadge/sqlx67
型安全な言語を利用する● 多くの言語は、GraphQLもgRPCも型やスキーマをごまかして使えてしまうので、クライアントからするとクエリやメッセージ通りの値を受け取れる保証がない● 自分がサーバーを実装するなら Rust で書きたい!!!!!!!!!● Rustの一番好きな点は、「型を誤魔化すコストが高く、誤魔化すモチベーションがなくなる」ところ。any や ts-ignore 的な抜け道が塞がれている。Rustで書かれたサーバーはそれだけでフロントエンドエンジニア視点で信用できて、過剰な防御をやめる動機になる!!!68
っていうのは全部妄想です● まとめ①: スキーマレスで動いてしまってるものにスキーマを入れるのは難しいです。フルリプレイスは救いですが、現実的にフルリプレイスなんてものは簡単にできるわけがないので、できるところからやっていきましょう。妥協するには JSON Schema が良いです● まとめ②: しかし妥協するからにはコンピューターフレンドリーで自動化された運用フローに期待できず、人間が運用フローを作らないといけないです● そんな運用フローを一緒に整備してくれる方を募集しています「散々レガシーなこと話しておいて人が来ると思ってるの?」と思うかもしれませんが、どこの会社にもレガシーはあるはずだし悪いことだとは思っていないです。むしろそういった過去の遺産を、技術力や組織の力でどう未来に繋げていくかという仕事はクリエイティブで面白いと思いますし、自分も楽しいです。面白そうと思った方はぜひカジュアル面談しましょう。もちろんモダン環境(Nix, Rust, Scala)でひたすら楽しい開発ができる仕事もあります。是非カジュアル面談をしましょう。69