GraphQL と gRPC で通信する Go 製のマイクロサービスをセットアップする

## 背景
年末年始に Go を勉強したので、 GraphQL と gRPC のサンプル実装を作ってみた。

## 作りたいもの
- フロントエンドから gateway 間を GraphQL で通信する
- gateway とバックエンドサービス間を gRPC で通信する

```mermaid
graph TD;
    ブラウザ --> graphql-gateway
    graphql-gateway --> grpc-user-service
```

## 前提知識
### GraphQL
- 独自のクエリ言語を用いて、フロントエンドで必要なリソース・プロパティだけを柔軟に取得できる
- スキーマ定義をフロントエンドとバックエンドで共有できるので変更にも強い
- https://graphql.org

### gRPC
- 独自のプロトコルを用いてバックエンドサービス間の高速な通信を実現できる
- 同じランタイム内の関数を呼ぶ感覚でバックエンドの処理を呼ぶことができ、様々な言語・フレームワークで実装されたサービスをシームレスに接続できる
- スキーマ定義をクライアントとバックエンドで共有できるので変更にも強い
- https://grpc.io

## 環境
- Go 1.19.3

## GraphQL 開発環境の構築
下記のようなディレクトリ構成で進める。

```tree
golang-web-server-sample/
├── docker-compose.yml
├── graphql-gateway/ # フロントエンドから直接リクエストを受け取る GraphQL 製サービス
│   └── Dockerfile
├── grpc-user-service/ # gRPC 製バックエンドサービス
│   └── Dockerfile
└── proto/ # gRPC 用のスキーマ定義（protocol buffers）からスタブを生成するためのコンテナ
    └── Dockerfile
```

今回は複数サービスを手元で立ち上げられるように docker-compose を用いて開発環境を構成していく。
まずは graphql-gateway をビルドする環境を作る。

```yaml:docker-compose.yml
version: "3"
services:
  graphql-gateway:
    build: ./graphql-gateway
    volumes:
      - .:/app
    ports:
      - "8080:8080"
    depends_on:
      - grpc-user-service
```

ポイントは、他のサービスのディレクトリを含む全てのファイルを Docker 内の作業用ディレクトリ /app にマウントしているところ。
これは gRPC の設定時に詳しく説明するが、 gRPC 用のスキーマ定義(protocol buffers)を各サービスから symbolic link で参照できるようにするためにこうしている。

次に graphql-gateway 用の Dockerfile を用意していく。

```dockerfile:graphql-gateway/Dockerfile
FROM golang:1.19.3

WORKDIR /app/graphql-gateway

COPY go.mod go.sum ./
RUN go mod download

RUN go install github.com/cosmtrek/air@v1.40.4
RUN go install github.com/99designs/gqlgen@v0.17.22
```

go.mod と go.sum が無いとビルド時にエラーが発生するので、先に作成しておく。

```diff:graphql-gateway/go.mod
module github.com/midwhite/golang-web-server-sample/graphql-gateway

go 1.19
```

module 宣言のレポジトリ部分は適宜自分のレポジトリ名に読み替えること。
go.sum は空ファイルで良いので touch コマンドで作っておく。

```sh
$ touch graphql-gateway/go.sum
```

ここまで来ると docker-compose でコンテナをビルドできるので、以降はコンテナ内で作業していく。
下記コマンドでコンテナ内に入ることができる。

```sh
$ docker-compose build
$ docker-compose run --rm graphql-gateway bash
```

go はデフォルトの状態だと live reload してくれなくて不便なので [air](https://github.com/cosmtrek/air) を用いてコードの変更が即反映されるようにしていく。

```sh
$ air init

  __    _   ___
 / /\  | | | |_)
/_/--\ |_| |_| \_ , built with Go

.air.toml file created to the current directory with the default settings
```

`air init` コマンドで air の設定ファイル `.air.toml` を生成する。
生成したファイルの `cmd` 部分を、今回のエントリーポイントを置く予定の場所に書き換える。

```diff:graphql-gateway/.air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "./tmp/main"
-  cmd = "go build -o ./tmp/main ."
+  cmd = "go build -o ./tmp/main ./server.go"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  exclude_file = []
  exclude_regex = ["_test.go"]
  exclude_unchanged = false
  follow_symlink = false
  full_bin = ""
  include_dir = []
  include_ext = ["go", "tpl", "tmpl", "html"]
  kill_delay = "0s"
  log = "build-errors.log"
  send_interrupt = false
  stop_on_error = true

[color]
  app = ""
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  time = false

[misc]
  clean_on_exit = false

[screen]
  clear_on_rebuild = false
```

これで `air` コマンドで起動すればソースコードの変更が即反映される状態になったので、開発環境の構築はここまでにして次に進める。


## gqlgen の導入
Go 用の GraphQL ライブラリとして [gqlgen](https://gqlgen.com) を利用する。
まずは下記コマンドで初期ファイルを生成する。

```bash
$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init
```

下記のようなファイルが生成される。

```
graphql-gateway/
├── gqlgen.yml
├── graph
│   ├── generated.go
│   ├── model
│   │   └── models_gen.go
│   ├── resolver.go
│   ├── schema.graphqls
│   └── schema.resolvers.go
└── server.go
```

ここまでで docker-compose を使ってサーバーが立ち上がる状態になったので、コンテナから抜けて docker-compose up を実行してみよう。

```sh
$ docker-compose up -d
[+] Running 2/2
 ⠿ Network golang-web-server-sample_default              Created
 ⠿ Container golang-web-server-sample-graphql-gateway-1  Started
$ docker-compose logs
golang-web-server-sample-graphql-gateway-1  |
golang-web-server-sample-graphql-gateway-1  |   __    _   ___
golang-web-server-sample-graphql-gateway-1  |  / /\  | | | |_)
golang-web-server-sample-graphql-gateway-1  | /_/--\ |_| |_| \_ , built with Go
golang-web-server-sample-graphql-gateway-1  |
golang-web-server-sample-graphql-gateway-1  | watching .
golang-web-server-sample-graphql-gateway-1  | watching graph
golang-web-server-sample-graphql-gateway-1  | watching graph/model
golang-web-server-sample-graphql-gateway-1  | !exclude tmp
golang-web-server-sample-graphql-gateway-1  | building...
golang-web-server-sample-graphql-gateway-1  | running...
golang-web-server-sample-graphql-gateway-1  | 2023/01/06 23:23:26 connect to http://localhost:8080/ for GraphQL playground
```

またブラウザで http://localhost:8080/graphql にアクセスしてみると、手元でクエリを作って実行できる GraphiQL の画面が表示されるはずだ。
![スクリーンショット 2023-01-07 8.26.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/a62d4df7-910b-2a84-996b-6580b093d274.png)

## GraphQL の実装
gqlgen を用いた GraphQL の実装は下記のように進める。

- `graph/schema.graphqls` にスキーマ定義を書き、それを元にモデルとリゾルバを自動生成する
- 自動生成されたリゾルバに必要な処理を実装する

まずスキーマ定義を下記のように書いてみる。

```graphql:graphql-gateway/graph/schema.graphqls
type User {
  id: ID!
  name: String!
  age: Int!
}

type Query {
  users: [User!]!
}

input NewUser {
  name: String!
  age: Int!
}

input UserAttributes {
  id: ID!
  name: String!
  age: Int!
}

type Mutation {
  createUser(input: NewUser!): User!
  updateUser(input: UserAttributes!): User!
}
```

次に model と resolver を自動生成するが、最初に生成されたファイルにあった定義を消してしまったので、ファイルを1回削除してから再生成する。

```sh
$ rm graph/schema.resolvers.go
$ gqlgen # このコマンドで model, resolver を自動生成する
```

すると graph/ ディレクトリ以下のファイルが自動で編集され、必要な型などが追加される。
ここで graph/schema.resolvers.go を見ると `Users` `CreateUser` `UpdateUser` といった query/mutation が下記のように生成されている。

```go:graphql:graphql-gateway/graph/schema.resolvers.go
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	panic(fmt.Errorf("not implemented: CreateUser - createUser"))
}

// UpdateUser is the resolver for the updateUser field.
func (r *mutationResolver) UpdateUser(ctx context.Context, input model.UserAttributes) (*model.User, error) {
	panic(fmt.Errorf("not implemented: UpdateUser - updateUser"))
}

// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	panic(fmt.Errorf("not implemented: Users - users"))
}
```

この中身を手で実装していくことになるが、現時点ではまだバックエンドサービスを作っていないので、仮で下記のように実装する。

```go:graphql:graphql-gateway/graph/schema.resolvers.go
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	return &model.User{ID: "uuid", Name: input.Name, Age: input.Age}, nil
}

// UpdateUser is the resolver for the updateUser field.
func (r *mutationResolver) UpdateUser(ctx context.Context, input model.UserAttributes) (*model.User, error) {
	return &model.User{ID: input.ID, Name: input.Name, Age: input.Age}, nil
}

// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	users := []*model.User{
		{ID: "1", Name: "徳川家康", Age: 20},
		{ID: "2", Name: "豊臣秀吉", Age: 25},
		{ID: "3", Name: "織田信長", Age: 30},
	}

	return users, nil
}
```

試しに http://localhost:8080/graphql でクエリを投げてみると、下記の通り期待した結果が得られる。
![スクリーンショット 2023-01-07 13.10.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/c732d7f3-1def-ffb9-9546-b3f2bc070c66.png)
![スクリーンショット 2023-01-07 13.15.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/9a0e6054-d335-e133-7a9a-5d51a0ad9e7c.png)
![スクリーンショット 2023-01-07 13.16.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/f0de2588-842a-82a3-8be6-a086da8adfe0.png)

ここまでで GraphQL の実装は終わりにして、バックエンドサービスを作る段階に進むことにする。

## Protocol Buffers 開発環境の構築
gRPC で開発を始めるために、 Protocol Buffers で書かれたスキーマ定義から gRPC の実装を自動生成する土台を作っていく。
まずは docker-compose に必要なサービスを追加していく。

```diff:docker-compose.yml
version: "3"
services:
  graphql-gateway:
    build: ./graphql-gateway
    volumes:
      - .:/app
    ports:
      - "8080:8080"
    command: air
    depends_on:
      - grpc-user-service
+  proto:
+    build: ./proto
+    volumes:
+      - .:/app
```

これはスキーマ定義からサービス間通信に用いる実装を自動生成するためのコンテナで、開発時にのみ実行する。
これをビルドするための Dockerfile を下記のように書く。

```dockerfile:proto/Dockerfile
FROM golang:1.19.3

WORKDIR /app/proto

RUN apt-get update && apt-get install -y protobuf-compiler

COPY go.mod go.sum ./
RUN go mod download
```

次に前回と同様 go.mod と go.sum を先に作る。

```:proto/go.mod
module github.com/midwhite/golang-web-server-sample/proto

go 1.19
```

```sh
$ touch proto/go.sum
```

docker コンテナを立ち上げ、中で作業する。

```sh
$ docker-compose build
$ docker-compose run --rm proto bash
```

必要な go のライブラリをインストールする。

```sh
$ go get github.com/golang/protobuf/protoc-gen-go@v1.5.2
```

これで go.mod/go.sum に protoc-gen-go が追加されたので、 Dockerfile に下記を追加してイメージをビルドし直す。

```diff:proto/Dockerfile
FROM golang:1.19.3

WORKDIR /app/proto

RUN apt-get update && apt-get install -y protobuf-compiler

COPY go.mod go.sum ./
RUN go mod download
+
+ RUN go install github.com/golang/protobuf/protoc-gen-go@v1.5.2
+ 
+ CMD ["make", "generate"]
```

```sh
$ docker-compose build
```

スキーマ定義ファイルを下記のように追加する。
※レポジトリ部分は適宜自分のレポジトリ名に読み替えること。

```proto:proto/user-service/user-service.proto
syntax = "proto3";

option go_package = "github.com/midwhite/golang-web-server-sample/grpc-user-service/pb";

package pb;

service UserService {
  rpc GetUserDetail(GetUserDetailParams) returns (UserDetail) {}
}

message GetUserDetailParams { string id = 1; }

message UserDetail {
  string id = 1;
  string name = 2;
  int64 age = 3;
}
```

文法については[公式docs](https://developers.google.com/protocol-buffers/docs/proto3)を参照のこと。
このスキーマ定義ファイルから gRPC の実装を生成するために下記の通り Makefile を追加する。

```makefile:proto/Makefile
generate:
	protoc -I . ./user-service/user-service.proto --go_out=plugins=grpc:./user-service
```

これを実行する。

```sh
$ make generate
protoc -I . ./user-service/user-service.proto --go_out=plugins=grpc:./user-service
```

すると `user-service/github.com/midwhite/golang-web-server-sample/grpc-user-service/pb/` という深いディレクトリに `user-service.pb.go` というファイルが生成される。
このファイル内に gRPC 通信用のクライアント実装とサーバー実装が含まれているので、それを各サービスから参照して利用していく。

## gRPC サーバー開発環境の構築
例の通り docker-compose にサービスを追加し、 Dockerfile を追加し、 go.mod/go.sum を作成する。

```diff:docker-compose.yml
version: "3"
services:
  graphql-gateway:
    build: ./graphql-gateway
    volumes:
      - .:/app
    ports:
      - "8080:8080"
    command: air
    depends_on:
      - grpc-user-service
  proto:
    build: ./proto
    volumes:
      - .:/app
+  grpc-user-service:
+    build: ./grpc-user-service
+    volumes:
+      - .:/app
+    command: air
```

```dockerfile:grpc-user-service/Dockerfile
FROM golang:1.19.3

WORKDIR /app/grpc-user-service

RUN apt-get update && apt-get install -y postgresql-client

COPY go.mod go.sum ./
RUN go mod download

RUN go install github.com/cosmtrek/air@v1.40.4
```

```:grpc-user-service/go.mod
module github.com/midwhite/golang-web-server-sample/grpc-user-service

go 1.19
```

```sh
$ touch grpc-user-service/go.sum
```

イメージをビルドして docker コンテナの中に入る。

```sh
$ docker-compose build
$ docker-compose run --rm grpc-user-service bash
```

air の設定ファイルを生成する。

```
$ air init
```

ここまでで開発環境の構築は済んだので、gRPC サーバーの実装に進む。

## gRPC サーバーを実装する
まずは pb ディレクトリを作り、先ほど proto で生成したファイルに symbolic link を貼る。

```sh
$ mkdir pb
$ cd pb/
$ ln -s ../../proto/user-service/github.com/midwhite/golang-web-server-sample/grpc-user-service/pb/user-service.pb.go userservice.go
```

こうすることで `grpc-user-service/pb/userservice.go` の位置で先ほど生成したファイルを参照することができる。
そこで新しく `impl/userservice.go` を追加し、下記のように先ほど protocol buffers で定義したスキーマの通りに中の処理を実装していく。
※今回は gRPC を介した通信さえできれば良いので、処理の中身は雑に固定の値を返すだけにする。

```go:grpc-user-service/impl/userservice.go
package impl

import (
	"context"

	"github.com/midwhite/golang-web-server-sample/grpc-user-service/pb"
)

type UserServiceServer struct{}

func (s *UserServiceServer) GetUsers(_ context.Context, _ *pb.GetUsersParams) (*pb.UserList, error) {
	users := []*pb.User{
		{Id: "1", Name: "北条氏康", Age: 20},
		{Id: "2", Name: "武田信玄", Age: 30},
		{Id: "3", Name: "今川義元", Age: 40},
	}
	response := pb.UserList{
		Users: users,
	}
	return &response, nil
}

func (s *UserServiceServer) CreateUser(_ context.Context, _ *pb.CreateUserParams) (*pb.User, error) {
	user := pb.User{
		Id:   "uuid",
		Name: "長尾景虎",
		Age:  25,
	}
	return &user, nil
}

func (s *UserServiceServer) UpdateUser(_ context.Context, params *pb.UpdateUserParams) (*pb.User, error) {
	user := pb.User{
		Id:   params.Id,
		Name: "上杉謙信",
		Age:  35,
	}
	return &user, nil
}
```

このサーバー実装をエンドポイントとして公開するため下記のようにエントリーポイントを実装する。
（[公式のサンプル実装](https://github.com/grpc/grpc-go/blob/master/examples/route_guide/server/server.go)に倣っただけ）

```go:grpc-user-service/server.go
package main

import (
	"flag"
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"

	"github.com/midwhite/golang-web-server-sample/grpc-user-service/impl"
	"github.com/midwhite/golang-web-server-sample/grpc-user-service/pb"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf("grpc-user-service:%d", *port))

	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	var opts []grpc.ServerOption
	grpcServer := grpc.NewServer(opts...)
	pb.RegisterUserServiceServer(grpcServer, &impl.UserServiceServer{})
	grpcServer.Serve(lis)
}
```

後は必要なライブラリをインストールすれば起動可能な状態になる。

```sh
$ go mod tidy
```

## gRPC クライアントを実装する
最後に graphql-gateway から grpc-user-service に接続するため、 gRPC クライアントを実装していく。
と言っても、 graphql-gateway 側でも proto 内の user-service.pb.go を参照して、クライアント実装にパラメータを渡すだけで済むので簡単だ。

まずは下記のように symbolic link を作成する。

```sh
$ mkdir /app/graphql-gateway/pb
$ cd /app/graphql-gateway/pb
$ ln -s ../../proto/user-service/github.com/midwhite/golang-web-server-sample/grpc-user-service/pb/user-service.pb.go userservice.go
```

次に gRPC クライアントを実装する。
（これも[公式のサンプル実装](https://github.com/grpc/grpc-go/blob/master/examples/route_guide/client/client.go)に倣っただけ）

```go:graphql-gateway/userservice/client.go
package userservice

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/midwhite/golang-web-server-sample/graphql-gateway/pb"
)

var Client pb.UserServiceClient

func Setup() (func(), error) {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))

	conn, err := grpc.Dial("grpc-user-service:50051", opts...)
	if err != nil {
		return nil, err
	}
	Client = pb.NewUserServiceClient(conn)

	return func() {
		conn.Close()
	}, nil
}
```

この Setup 関数をエントリーポイントの server.go 内から呼び出す。

```diff:graphql-gateway/server.go
package main

import (
	"log"
	"net/http"
	"os"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/midwhite/golang-web-server-sample/graphql-gateway/graph"
	"github.com/midwhite/golang-web-server-sample/graphql-gateway/userservice"
)

const defaultPort = "8080"

func main() {
+	closer, err := userservice.Setup()
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer closer()

	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))

	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
	http.Handle("/query", srv)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}
```

最後に GraphQL のリゾルバから gRPC クライアントを呼び出す。

```go:graphql-gateway/graph/schema.resolvers.go
package graph

import (
	"context"

	"github.com/midwhite/golang-web-server-sample/graphql-gateway/graph/model"
	"github.com/midwhite/golang-web-server-sample/graphql-gateway/pb"
	"github.com/midwhite/golang-web-server-sample/graphql-gateway/userservice"
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	params := pb.CreateUserParams{Name: input.Name, Age: int64(input.Age)}
	res, err := userservice.Client.CreateUser(ctx, &params)

	if err != nil {
		return nil, err
	}

	user := model.User{ID: res.Id, Name: res.Name, Age: input.Age}

	return &user, nil
}

func (r *mutationResolver) UpdateUser(ctx context.Context, input model.UserAttributes) (*model.User, error) {
	params := pb.UpdateUserParams{Id: input.ID, Name: input.Name, Age: int64(input.Age)}
	res, err := userservice.Client.UpdateUser(ctx, &params)

	if err != nil {
		return nil, err
	}

	user := model.User{ID: res.Id, Name: res.Name, Age: input.Age}

	return &user, nil
}

func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	res, err := userservice.Client.GetUsers(context.Background(), &pb.GetUsersParams{})

	if err != nil {
		return nil, err
	}

	users := make([]*model.User, len(res.Users))

	for i, user := range res.Users {
		users[i] = &model.User{ID: user.Id, Name: user.Name, Age: int(user.Age)}
	}

	return users, nil
}

func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
```

これで実装は完了しているので、 http://localhost:8080/graphql で動作確認してみる。
![スクリーンショット 2023-01-07 18.28.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/e2b029f8-e7a0-e298-bbfc-2d41e2440ec9.png)
![スクリーンショット 2023-01-07 18.29.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/6c436477-2b9e-edae-faba-5e8d8d8f2771.png)
![スクリーンショット 2023-01-07 18.27.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/d0b759f9-99f0-d32e-a557-8ca53ada88ab.png)

無事に長尾景虎が上杉謙信へと更新されていることが分かる。
ここまでで、フロントエンドとの通信を GraphQL で行い、バックエンドとの通信を gRPC で行う Go 製のマイクロサービスの開発環境をセットアップすることができた。

## あとがき
本来は他にも DB との接続設定や運用時の監視設定やテストの設定など色々やるべきことはあるが、今回はサービス間通信のみを切り取って説明した。
まだ Go を学習して日が浅いため、例えば protocol buffers から Go 以外のスタブを生成する時にこのディレクトリ構成はこれでいいのか、 Go の gRPC クライアントの初期化処理はこれでいいのか等、実装全体で疑問に思っていることは多いものの、とりあえず動いたのでヨシ！

![jrRD7Uvn_400x400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131942/d8b74532-0556-ec59-4deb-14a9aa984521.png)

お気づきの点があればコメントで教えていただけると嬉しいです。 mm