Closed10
Apollo Federation を理解したい
概念理解
- Why GraphQL Federation?
- Netflix の記事が有名っぽいので読んでみる
The goal of GraphQL Federation is two-fold: provide a unified API for consumers while also giving backend developers flexibility and service isolation. To achieve this, schemas need to be created and annotated to indicate how ownership is distributed.
- (gateway の裏側にいる microservice を1段階まとめる単位をどのように分割するかが肝になりそう)
- architecture components
- gateway = (Apollo 公式ドキュメントでいう supergraph)
- DGS (Domain Graph Service) = (Apollo 公式ドキュメントでいう subgraph)
- schema registry
- key concepts in implementation
- schema composition
- query planning and execution
- entity resolver
Apollo 公式ドキュメントを読んでみる
Federated Schemas
Schema composition
- subgraph の schema を supergraph schema に合成する
- 異なる subgraph 間で同じフィールドに対して異なる定義をしている場合は validation error になる
- ただし optional かどうかが異なるだけなら OK → この場合 nullable になる
-
@shareable
を付けると複数 subgraph で同一フィールドを定義できるようになる
-
If multiple subgraphs define the same type, each field of that type must be resolvable by every valid GraphQL operation that includes it.
- subgraph 間での互換性を気にする必要がある
-
@key
を付けて type を Entity として扱うようにすると、1つの type に関する定義を異なる subgraph 間で分割してできるThis is a useful solution when a type corresponds closely to an entry in a data store that one or more of your subgraphs has access to (e.g., a Users database).
- merge の戦略は Union (全部含める) と Intersection (共通項を取る) の2つが存在 → どちらを採用するかは GraphQL の型に依存
- object, union, interface は常に Union
- input, field arguments は常に Intersection
- Enum の場合は使われ方による
- (受け取る値は厳しく/返す値は緩く、という方針)
Entity
an entity is an object type that can resolve its fields across multiple subgraphs.
To fully define an entity within a single subgraph, you do the following:
- Assign the entity a
@key
- Define the entity's reference resolver
reference resolver とは
The @key directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its primary key." In order for this to be true, the subgraph needs to define a reference resolver for the entity.
- Entity に対して1つでも独自のフィールドを定義している subgraph は reference resolver を実装する必要がある
- どのように reference resolver を定義するかは利用しているライブラリによって異なるので個別に調査が必要
- Entity を参照するだけなら schema 内に stub を定義すれば reference resolver の実装を省略できる
-
@key
を指定 &resolvable: false
として ID のみの型を定義しておくと query plan 時に__typename
,id
を元に別の subgraph が実装している reference resolver を呼び出して必要なフィールドを解決してくれる
-
federation 特有の directive 定義
- 型定義
-
@key
: Entity を一意に特定するキーを指定する -
@extends
: 型の拡張を行う場合に指定する- federation 2 では利用しない
-
- フィールドの共有
-
@shareable
: あるフィールドが複数の subgraph によって解決可能であることを示す -
@inaccessible
: supergraph の schema から除外したいフィールドに対して指定する-
@shareable
なフィールドを段階的に実装する際や client に対して private なフィールドを@key
の一部として扱いたい場合に利用する
-
-
@override
: ある subgraph で定義していたフィールドを別の subgraph で定義し直していることを示す- subgraph 間でフィールド定義のマイグレーションを行う際に利用する
-
- フィールドの参照
-
@external
: その subgraph では対象のフィールドを解決できないが、何らかの理由で型としては定義しておきたい場合に指定する-
@provides
や@requires
と組み合わせて利用する
-
-
@provides
: 特定の schema の path でのみ解決可能なフィールド群を指定する- クエリの最適化のために利用する
-
@requires
: あるフィールドの解決に他の subgraph で定義されているフィールドの解決が先に必要な場合に指定する- ここで指定されたフィールド群は client がリクエストしていなくても router 側でリクエストが行われる
-
- メタデータの定義
-
@tag
: 任意の string のメタデータを指定したフィールドに付与する
-
- custom directive の利用
-
@composeDirective
: subgraph で定義した custom directive を supergraph の schema でも残したい場合に指定する- デフォルトでは supergraph の schema に composition する際にほとんどの directive は除外される
-
Router
- supergraph を用意するには2つ方法がある: https://www.apollographql.com/docs/federation/#server-instances
- Apollo Router
- 色々良いところあるよ: https://www.apollographql.com/blog/announcement/backend/announcing-apollo-router-v1-0/
- Rust のバイナリをセルフホストする
- 公式の docker image がある: https://www.apollographql.com/docs/router/containerization/docker/
- helm chart もある: https://www.apollographql.com/docs/router/containerization/kubernetes/
- managed なサービスを利用する
- https://www.npmjs.com/package/@apollo/gateway を使って GraphQL サーバーを router として利用する
- Apollo Router
- Apollo Router を使った実装例
- https://github.com/apollographql/supergraph-demo-fed2
- (ちょうど触ってみたいと思っていた Rhai を使ってカスタマイズできるのが個人的にアツい)
lightweight Rhai scripting to customize the stock Router image
-
@apollo/gateway
on Next.js API Routes の実装事例↓
Rover
- 独自の CLI ツールが提供されており schema 管理に活用できる
Apollo Studio. This is the primary web interface for GraphOS. Studio helps you monitor, manage, and collaborate on your supergraph.
The Rover CLI. This is the primary command-line interface for GraphOS. Rover helps you interact with your graphs and their schemas.
- CI に組み込んで schema のバリデーションを行ったり: https://www.apollographql.com/docs/rover/ci-cd
- subgraph/supergraph それぞれを操作するためのコマンドが用意されている
- 例:
rover supergraph compose --config PATH_TO_CONFIG_FILE
で supergraph schema の composition ができる
- 例:
Managed Federation
(ここは一旦読み飛ばし)
実装してみる
TypeScript, Rust, Python の各言語で subgraph となる GraphQL サーバーを実装 + supergraph は @apollo/gateway
による自前実装で用意してみる。
できたもの↓
参考: 言語・ライブラリごとに使える機能の一覧
TypeScript
- NestJS +
@apollo/subgraph
package で実装 - reference resolver は
@ResolveReference
デコレータを付与したメソッドで定義する - 各種 directive は
@Directive
デコレータで付与する -
autoSchemaFile
オプションにfederation: 2
を指定して生成した schema 定義ファイルに directive が上手く反映されていなさそうな点が少し気になった- 実際に supergraph 経由でリクエストを送ってみると Entity の解決は上手くいっていた → 今回は supergraph の composition 時に subgraph の schema を動的に読み取りにいくように実装したため、生成された schema 定義ファイルは参照されていない
Rust
- async-graphql が Apollo Federation 対応しているので採用
- GraphQL サーバーの実装について
- https://zenn.dev/mkazutaka/articles/9b9228da5a741a が参考になった
- GraphQL schema を抽象化した
Schema
型があってQuery
,Mutation
,Subscription
をそれぞれ登録していく -
SimpleObject
trait を derive するとその型のフィールドは自動的に GraphQL の type にマッピングされる -
#[Object]
attribute を付与した型はフィールドごとに手動で resolver を定義する必要がある- root となる
Query
,Mutation
はこれで定義する
- root となる
- federation 関連
-
#[graphql(hoge)]
という attribute をフィールド単位に付けていくことで各種 directive の機能を担保する形式 -
#[graphql(entity)]
で@key
directive が付与され reference resolver が実装される - 他の各種 directive も同じように定義できる:
#[gaphql(shareable)]
→@shaeable
,#[graphql(extenal)]
→@external
など
-
Python
- strawberry-graphql が Apollo Federation 対応かつ code first に実装できるので採用
- dataclass を拡張したデコレータで GraphQL の type 定義を行う
- コマンドで schema 定義ファイルの出力もできて便利
- federation 関連
- 通常は
@strawberry.type
といったデコレータを用いるところを federation に対応させる場合は@strawberry.federation.type
といった形で定義していく - reference resolver はクラスメソッドで定義する
- 各種 directive はデコレータの引数で指定することで付与できる
- 通常は
一通り実装してみての感想
- Entity がコアな概念なのでちゃんと理解することが重要
- reference resolver が少し難しかったが「router から ID (= Entity を一意に特定する情報) が与えられた際に、その subgraph 内で対象 Entity をどのように解決すべきかを定義するもの」と捉えると理解できた
- schema 定義の際に supergraph first にするか subgraph first にするか問題
- ゼロから作る際には、前者の方が全体であるべき姿を描いてから最適な分割方法を設計できるので良さそうだが、現実には既に運用されている複数の GraphQL サーバーを subgraph として schema composition するケースも多そう
- 後者の場合それぞれの subgraph を別々のチームが開発する前提だと、同一 Entity のフィールド定義に関して複数 subgraph が責務を負うケースでフィールド間の調整が大変そうなので CI でバリデーションを行うなどの工夫が必要 → こういったケースで Rover を活用すると良さそう
- この辺は実運用で上手くやっている事例があったら知りたい
このスクラップは2023/01/14にクローズされました