🎃

GoでGraphQLでfederation

2021/12/09に公開

はじめに

趣味と業務でGo * GraphQLでコードを書いているソフトウェアエンジニアです。
そういやGoでのfederationやってみたいなーと思い、調べていたら、brambleという日本語の記事で見かけないGo製のfederation gatewayがあったのでラフに共有する記事です。

今回行った実装はここにあります
https://github.com/Sntree2mi8/go-graphql-federation-gateway

間違いなどあればご指摘いただけると幸いです。

今回やること

brambleの簡単な紹介と、federation gatewayにbrambleとfederated serverにgqlgenをを用いたall Go GraphQL federationをサッと実装してみます。

brambleとは

https://movio.github.io/bramble/#/
Go製のproduction-ready federation gateway。

実際に使ってみた感想でいえば使用感でいえば @apollo/gatewayと遜色なくとっても簡単にfederation gatewayを立てることができていい感じでした。
Apollo federationの仕様が必要以上に複雑という点から、brambleで独自の実装をしているので、対応していないfederated serverは自前で実装をする必要があります。

gqlgenとは

https://gqlgen.com/

GraphQLのGo製ライブラリの一つ。同じGo製のGraphQLライブラリの中では機能が一番多い。
Schema fisrtでの開発。ドキュメントも充実している。
https://gqlgen.com/feature-comparison/

自分は普段はgqlgenで開発をしています。
今回は紹介しませんが、gqlgenでApollo federation仕様のSchemaを生成することもできる。

https://gqlgen.com/recipes/federation/

とっても簡単にできるので是非お試ししてみてください。おったまげます。
公式のサンプルではfederation gatewayに @apollo/gatewayを用いています。

とりあえず動かしてみる

何はともあれ動かしてみました。
色々と書く必要があるのはfederated serverの方なので、最初にサクッとできるgatewayの方からです。

構成

brambleで実装したfederation gateway (:8082)と

その後ろにgqlgenで実装したuser service(:4001)とreport service(:4000)

federation gateway (bramble)

{
  "services": [
    "http://localhost:4000/query",
    "http://localhost:4001/query"
  ]
}

まずは、各サービスのエンドポイントを書いて

go install github.com/movio/bramble/cmd/bramble@latest
bramble config.json

これで終わり
とっても簡単!!!!!!!!

または、自分でカスタムなプラグインを入れたい場合などはmoduleとして落としてきて、使うこともできて、公式にもExampleがあります

federated servers (gqlgen)

federated serverとして利用するためには、gatewayにschemaを知らせるための実装が必要になり、以下の情報を全てのサービスのSchemaに追加、実装してあげる必要があります。

type Service {
  name: String!
  version: String!
  schema: String!
}

type Query {
  service: Service!
}

user service

schemaは以下の様に定義して

type Service {
  name: String!
  version: String!
  schema: String!
}

type User {
  id: ID!
  name: String!
}

type Query {
  service: Service!
  users: [User!]!
}

gqlgenのお作法に従ってコード生成をして、自分で実装する部分を埋めたものが以下です

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
	"context"
	"strings"

	"github.com/Sntree2mi8/go-graphql-federation-gateway/user/graph/model"
	"github.com/vektah/gqlparser/v2/formatter"
)

func (r *queryResolver) Service(ctx context.Context) (*model.Service, error) {
	s := new(strings.Builder)
	f := formatter.NewFormatter(s)
	// parsedSchemaはgqlgenによって生成されるやつ。unexportedな変数ではありますが、ここでは時間短縮のために同じpackageに生成して利用している
	f.FormatSchema(parsedSchema)

	service := model.Service{
		Name:    "user-service",
		Version: "0.1.0",
		Schema:  s.String(),
	}
	return &service, nil
}

// 仮のresponse
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	return []*model.User{
		{
			ID:   "user:1",
			Name: "Federation Taro",
		},
		{
			ID:   "user:2",
			Name: "Go Taro",
		},
	}, nil
}

// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }

type queryResolver struct{ *Resolver }

上のコメントでも書いている通り、サービスのスキーマを返すところで、gqlgenで生成された変数を利用しているので、生成する場所はデフォルトから変更する必要があります。

report service

type Service {
  name: String!
  version: String!
  schema: String!
}

type Report {
  id: ID!
  content: String!
}

type Query {
  service: Service!

  reports: [Report!]!
}
package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
	"context"
	"strings"

	"github.com/Sntree2mi8/go-graphql-federation-gateway/report/graph/model"
	"github.com/vektah/gqlparser/v2/formatter"
)

func (r *queryResolver) Service(ctx context.Context) (*model.Service, error) {
	s := new(strings.Builder)
	f := formatter.NewFormatter(s)
	f.FormatSchema(parsedSchema)

	service := model.Service{
		Name:    "report-service",
		Version: "0.1.0",
		Schema:  s.String(),
	}
	return &service, nil
}

// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }

type queryResolver struct{ *Resolver }

report serviceの方も同様に実装

結果

各サーバーを動かしてfederation gatewayにクライアントを向けてみると

できた!!!簡単!!!

その他いじってみて気になったやつ

boundary directive

https://movio.github.io/bramble/#/federation?id=boundary-directive
brambleでは、boundary directiveという仕組みがあり、これを用いてサービス間でオブジェクト定義を共有することができます

user service schema

type User {
  id: ID!
  name: String!
  # ユーザーに紐づいたreportを返したい!!
  reports: [Report!]!
}

例えばこの様に、ユーザーに対して紐づいているreportを返したい時に、reportは違うサービスにて実装されているため、user serviceから返すことはあまりよろしくない場合に、

reportのschema.graphqls

directive @boundary on OBJECT | FIELD_DEFINITION

type Service {
  name: String!
  version: String!
  schema: String!
}

type Report @boundary {
  id: ID!
  content: String!
}

type Query {
  service: Service!

  reports: [Report!]!

  report(id: ID!): Report @boundary
}

userのschema.graphqls

directive @boundary on OBJECT | FIELD_DEFINITION

type Service {
  name: String!
  version: String!
  schema: String!
}

type User {
  id: ID!
  name: String!
  reports: [Report!]!
}

type Query {
  service: Service!

  users: [User!]!

  report(id: ID!): Report @boundary
}

type Report @boundary {
  id: ID!
}

この様に実装をすることで、

  • ユーザーサービスからは関連するreportIDだけを返すだけ
  • 具体的なreportの内容に関する実装はreport serviceに置いたまま

ユーザーに紐づくreportの情報を返すという目的を達成することができます。便利!!!!!!!! (具体的な実装はGitHubを参照してください)

これにより、user serviceはreportに関して気にすることが最小限になるので、結合度を下げることができそうです。
実装のやり方が悪いのか、introspectionでuser.reportsの情報が返ってきていないことは気になりますが、queryで指定するとちゃんと返ってきます。

まとめ

今回はbrambleを用いてAll GoでGraphQL federationを実装してみました。
完成まではApolloとも遜色ない速さでできるのでとっても使いやすかったです。
また、これ以外にも、boundary directiveという、依存小さめでサービス間でオブジェクトを共有できる仕組みがあり、バックエンドの各サービスをチーム毎に持たせているようなチーム体制だと依存少なく開発できて良さそうと思ったりしました。
まだ全然いじれていないのでもう少しいじってみてちゃんと感想は書きたいですね...

Discussion