grpc-gatewayってなに？実装しながら理解してみよう

株式会社Schoo 新卒1年目の [@hiroto_0411](https://x.com/hiroto_0411)です！
最近、gRPCサーバーの実装を行っていく中で、grpc-gatewayというのがあると知ったので実装しながら理解を深めてみました！

## この記事でわかること
- grpc-gatewayってなに？
- grpc-gatewayを使った、gRPCサーバーを作る流れ

gRPCについて理解が曖昧な方は以下の記事にまとめたのでこちらを先に見ていただくと、この記事が理解しやすくなると思います。

https://qiita.com/hiroto_0411/items/e87b626878d288460d62

## grpc-gatewayとは
.protoファイルを元に、REST APIに送られてきたリクエストを変換して、gRPCサーバーに転送するためのリバースプロキシーサーバーを作ることができるプラグイン。

**リバースプロキシーサーバー**
クライアントからのリクエストを別のサーバーに転送するサーバー

イメージ
![スクリーンショット 2024-10-17 20.52.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810533/2b1423f3-7f80-a593-e22c-d4a210924343.png)
grpc-gatewayでリバースプロキシーサーバーを作ることで、REST APIとgRPC APIどちらも実装できる。

後方互換性の維持、gRPCで十分にサポートされていない言語やクライアントのサポート、RESTの思想の方が合っているなど、gRPCだけでなくREST APIも提供したいとケースに対応するために作られた。

**参考**

https://github.com/grpc-ecosystem/grpc-gateway

## grpc-gatewayを使ってみる

https://github.com/grpc-ecosystem/grpc-gateway?tab=readme-ov-file

上記のREADMEを参考にしながらgrpc-gatewayを使ってみた。

作成したコードは以下で見られる。

https://github.com/hiroto1220/go-playground/tree/main/gRPC-gateway


ディレクトリ構成はこんな感じになる予定。
```
.
├── gateway
│   └── main.go
├── gen
│   ├── helloworld_grpc.pb.go
│   ├── helloworld.pb.go
│   └── helloworld.pb.gw.go
├── proto
│   └── helloworld.proto
├── server
│   └── helloworld.go
├── buf.gen.yaml
├── buf.lock
├── buf.yaml
├── go.mod
├── go.sum
└── main.go
```

### 1. 準備
**モジュールの作成**
```
go mod init github.com/user-name/project-name   
```
**コード生成に必要なツールのインストール**
```
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/bufbuild/buf/cmd/buf@latest
```

```
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
```
**PATHを更新**
```
export PATH="$PATH:$(go env GOPATH)/bin"
```

### 2. .protoファイルの作成
Protocol Buffersというフォーマットを使用してgRPCのサービス名やメソッド名、リクエストやレスポンスを定義する。

~~~./proto/helloworld.proto
syntax = "proto3";

option go_package = "github.com/user-name/project-name/gen";

import "google/api/annotations.proto";

package hello;

service Greeter {
    rpc SayHello(HelloRequest) returns (HelloResponse){
        option (google.api.http) = {
          post: "/hello"
          body: "*"
        };
  }
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}
~~~

この後、.protoファイルを元にコードを自動生成する。その時に出力したいpackage名を`go_package`で指定する。`package hello;`はGoのpackage名とは関係ない。このpackageは名前が衝突するのを防ぐために使われる。
```
package user;

message Person {
    string name = 1;
    int32 age = 2;
}
```
```
package employee;

message Person {
    string name = 1;
    string employee_id = 2;
}
```
このように同じ名前のserviceやmessageを定義したときに問題が発生しないように、package名を指定する。

### 3. コードの自動生成
**buf.yamlファイルをルートディレクトリに作成**
buf.yamlにはどのディレクトリの.protoファイルをコンパイルするかや、linterなどの設定、リモートのプロジェクトやサードパーティのProtobufファイルを参照するための依存関係の設定などを記述する。
```
buf config init
```

~~~./buf.yaml
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE
deps:
  - buf.build/googleapis/googleapis
~~~
`./proto/helloworld.proto`で`import "google/api/annotations.proto";`している依存関係を記述する。

**依存関係のロックファイルを更新**
初めてこのコマンドを実行するときはbuf.lockが作成される。
```
buf mod update
```

**buf.gen.yamlをルートディレクトリに作成**

~~~./buf.gen.yaml
version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt:
      - module=github.com/user-name/project-name/gen
  - local: protoc-gen-go-grpc
    out: gen
    opt: 
      - module=github.com/user-name/project-name/gen
  - local: protoc-gen-grpc-gateway
    out: gen
    opt:
      - module=github.com/user-name/project-name/gen
      - generate_unbound_methods=true
~~~

**コードを自動生成**
以下のコマンドを実行することで、.protoファイルを元に、XXX_grpc.pg.goとXXX.pb.go、XXX.pb.gw.goの3種類のファイルが自動生成される。

```
buf generate
```
**モジュールの依存関係を整理**
```
go mod tidy
```
### 4. 自動生成されたinterfaceを使い、gRPCサーバーを実装する

~~~./gen/helloworld_grpc.pb.go
type GreeterServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
	mustEmbedUnimplementedGreeterServer()
}

type UnimplementedGreeterServer struct{}

func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
~~~

~~~./server/helloworld.go
package server

import (
	"context"
	"log"

	pb "github.com/user-name/project-name/gen"
)

type GreeterServer struct {
	pb.UnimplementedGreeterServer
}

// ./gen/helloworld_grpc.pb.goに定義されているGreeterServerインターフェースを実装する
func (s *GreeterServer) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	log.Printf("Received: %v", in.GetName())

	// 本来ならここから、別の層のメソッドを呼び出すなどする

	return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil
}
~~~
[./gen/helloworld_grpc.pb.go](https://github.com/hiroto1220/go-playground/blob/main/gRPC-gateway/gen/helloworld_grpc.pb.go)に定義されているGreeterServerインターフェースを実装するメソッドを作成し、それをmain.goで使用する。

~~~./main.go
package main

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

	pb "github.com/user-name/project-name/gen"
	"github.com/user-name/project-name/server"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

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

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server.GreeterServer{})
	log.Printf("server listening at %v", lis.Addr())

	reflection.Register(s)

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
~~~

### 5. gatewayサーバーを実装する
~~~./gateway/main.go
package main

import (
	"context"
	"flag"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/grpclog"

	gw "github.com/user-name/project-name/gen"
)

var (
	// command-line options:
	// gRPC server endpoint
	grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50051", "gRPC server endpoint")
)

func run() error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Register gRPC server endpoint
	// Note: Make sure the gRPC server is running properly and accessible
	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
	if err != nil {
		return err
	}

	// Start HTTP server (and proxy calls to gRPC server endpoint)
	return http.ListenAndServe(":8081", mux)
}

func main() {
	flag.Parse()

	if err := run(); err != nil {
		grpclog.Fatal(err)
	}
}
~~~

### 6. それぞれのサーバーにリクエストを送ってみる
**サーバーの起動**
それぞれのmain.goがあるディレクトリに移動して起動する。
```
go run main.go
```

**grpcurlをインストール**
```
brew install grpcurl
```

**gRPCサーバーにリクエストを送信**
```
 grpcurl -plaintext -d '{"name": "パブりん"}' localhost:50051 hello.Greeter/SayHello
```
```
{
  "message": "Hello パブりん"
}
```

**gatewayサーバーにリクエストを送信**
```
curl \
    --header "Content-Type: application/json" \
    --data '{"name": "パブりん"}' \
    http://localhost:8081/hello   
```
```
{
  "message": "Hello パブりん"
}
```

## 参考

https://github.com/grpc-ecosystem/grpc-gateway?tab=readme-ov-file

https://future-architect.github.io/articles/20220624a/


---
**Schooでは一緒に働く仲間を募集しています！**

https://corp.schoo.jp/careers/graduates

https://corp.schoo.jp/careers