grpc-gatewayでgRPCのREST対応を試しました

## はじめに
初めまして、[ｋ．ｓ．ロジャース](https://www.wantedly.com/companies/ks-rogers)の西谷です。

自分自身はPHPで開発することが多いですが、他のプロジェクトではGoを利用しており、その中でREST APIからgRPCに移行する話が出てきています。
しかし、過渡期ということもあり、REST APIとgRPC両方の対応をする方針で進んでいます。
この場合はRESTとgRPCの実装を分けてしまうと、実装時も大変ですが、リリース後の改修等のメンテナンスコストも大きく上がるため、良くないと考えています。

そこで、[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway)を使えばgRPCをREST APIで叩けると聞いたため、今回検証をしてみようと思います。
今回は全体の流れを大まかに記載し、次回以降各部位の詳細について記載出来たらと思います。

間違い・助言等があればコメントにてお知らせいただけたらと思います。

## 環境構築
環境構築については他の記事にお任せしたいと思います。
本検証のために以下の記事が非常に参考になりました。
https://budougumi0617.github.io/2018/02/03/grpc-gateway-for-rest-api/
https://christina04.hatenablog.com/entry/2017/11/15/034455

## protbufでAPIの定義
今回はサンプルとしてユーザのCRUDを作ろうと思います。
つまり次のAPIを作成する必要があります。

|  | Method | URL | 役割 |
|------------|--------|------------------------------|----------------|
| ListUsers | GET | /api/v1/users | ユーザ一覧取得 |
| GetUser | GET | /api/v1/users/{encrypted_id} | 1ユーザを取得 |
| CreateUser | POST | /api/v1/users | ユーザ作成 |
| UpdateUser | PUT | /api/v1/users/{encrypted_id} | ユーザ更新 |
| DeleteUser | DELET | /api/v1/users/{encrypted_id} | ユーザ削除 |


そこで以下のprotoファイルを定義しました。

```protobuf:sample.proto
syntax = "proto3";

package sample;

import "google/api/annotations.proto";

service UserService {
    rpc ListUsers (ListUserRequest) returns (ListUsersResponses) {
        option (google.api.http) = {
          get: "/api/v1/users"
        };
    };

    rpc GetUser (GetUserRequest) returns (User) {
        option (google.api.http) = {
          get: "/api/v1/users/{encrypted_id}"
        };
    };

    rpc CreateUser (CreateUserRequest) returns (User) {
        option (google.api.http) = {
          post: "/api/v1/users"
          body: "*"
        };
    };

    rpc UpdateUser (UpdateUserRequest) returns (User) {
        option (google.api.http) = {
          put: "/api/v1/users/{encrypted_id}"
          body: "*"
        };
    };

    rpc DeleteUser (DeleteUserRequest) returns (Empty) {
        option (google.api.http) = {
          delete: "/api/v1/users/{encrypted_id}"
        };
    };
}

message Empty {
}

message ListUserRequest {
}

message GetUserRequest {
    string encrypted_id = 1;
}

message CreateUserRequest {
    string name = 1;
}

message UpdateUserRequest {
    string encrypted_id = 1;
    string name = 2;
}

message DeleteUserRequest {
    string encrypted_id = 1;
}

message User {
    string encrypted_id = 1;
    string name = 2;
}

message ListUsersResponses {
    repeated User users = 1;
}
```

上記ファイル作成後、以下コマンドでgRPC用のコードとgatewayのコードが生成されます。

```shell
protoc -I ./proto ./proto/sample.proto \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:./proto \
--go_out=plugins=grpc:proto
```


## gRPCサーバの実装
サーバはgolangで実装しました。
本来はDB接続を行いますが、今回はAPIの動作確認が目的のため変数で代用しました:bow:
また、エラー処理も一切含まれていません。

```golang:server/main.go
package main

import (
	"flag"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	pb "grpc-gateway/proto"
	"log"
	"math/rand"
	"net"
	"time"
)

// DBの代わり(手抜きすみません)
var USERS = map[string]string{"12345abcde": "hogehoge", "zxcvb09876": "fugafuga"}

type userService struct{}

// ユーザ一覧取得
func (e *userService) ListUsers(ctx context.Context, req *pb.ListUserRequest) (*pb.ListUsersResponses, error) {
	var users = []*pb.User{}
	for k, v := range USERS {
		users = append(users, &pb.User{EncryptedId: k, Name: v})
	}

	return &pb.ListUsersResponses{Users: users}, nil
}

// 1ユーザの取得(本来は詳細情報を付与して返す)
func (e *userService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
	return &pb.User{EncryptedId: req.EncryptedId, Name: USERS[req.EncryptedId]}, nil
}

// ユーザの作成
func (e *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
	encrypedId := GetRandString(10)
	USERS[encrypedId] = req.Name

	return &pb.User{EncryptedId: encrypedId, Name: req.Name}, nil
}

// ユーザの更新
func (e *userService) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.User, error) {
	USERS[req.EncryptedId] = req.Name

	return &pb.User{EncryptedId: req.EncryptedId, Name: req.Name}, nil
}

// ユーザ削除
func (e *userService) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.Empty, error) {
	delete(USERS, req.EncryptedId)
	return &pb.Empty{}, nil
}

// 乱数の生成(encrypted_id用)
func GetRandString(n int) string {
	var letterRunes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func main() {
	rand.Seed(time.Now().UnixNano())

	var port string
	flag.StringVar(&port, "port", "50000", "")
	flag.Parse()

	listen, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatalln(err)
	}

	server := grpc.NewServer()
	pb.RegisterUserServiceServer(server, &userService{})
	reflection.Register(server)

	if err := server.Serve(listen); err != nil {
		log.Fatalln(err)
	}
}
```
`go run server/main.go`などのコマンドで実行できます。

## gRPCクライアント
REST APIの前にgRPCのクライアントを実装して動作確認します。

```golang:client/main.go
package main

import (
	"bufio"
	"flag"
	"fmt"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"log"
	"os"
	"time"

	pb "grpc-gateway/proto"
)

// 全ユーザ取得
func ListUser(conn *grpc.ClientConn)  {
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.ListUsers(ctx, &pb.ListUserRequest{	})
	if err != nil {
		log.Fatalln(err)
	}

	for _, v := range resp.Users {
		log.Printf("EncryptedId: %s, Name: %s\n", v.EncryptedId, v.Name)
	}
}

// 1ユーザ取得
func GetUser(conn *grpc.ClientConn, encryptedId string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.GetUser(ctx, &pb.GetUserRequest{EncryptedId:encryptedId})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ作成
func CreatUser(conn *grpc.ClientConn, name string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.CreateUser(ctx, &pb.CreateUserRequest{Name:name})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ更新
func UpdateUser(conn *grpc.ClientConn, encryptedId string,name string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.UpdateUser(ctx, &pb.UpdateUserRequest{EncryptedId:encryptedId, Name:name})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ削除
func DeleteUser(conn *grpc.ClientConn, encryptedId string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	_, err := client.DeleteUser(ctx, &pb.DeleteUserRequest{EncryptedId:encryptedId})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("deleted")
}

// 標準入力受付
func GetStdInput(message string) string {
	fmt.Printf(message)

	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()

	return scanner.Text()
}

func main() {
	var addr string
	flag.StringVar(&addr, "addr", "localhost:50000", "")
	flag.Parse()

	conn, err := grpc.Dial(addr, grpc.WithInsecure())
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

L:
	for true {
		cmd := GetStdInput("Input ListUsers/GetUser/CreateUser/UpdateUser/DeleteUser/exit\n")

		switch cmd {
		case "ListUsers":
			ListUser(conn)
		case "GetUser":
			encryptedId := GetStdInput("encryptedId:")
			GetUser(conn, encryptedId)
		case "CreateUser":
			name := GetStdInput("name:")
			CreatUser(conn, name)
		case "UpdateUser":
			encryptedId := GetStdInput("encryptedId:")
			name := GetStdInput("name:")
			UpdateUser(conn, encryptedId, name)
		case "DeleteUser":
			encryptedId := GetStdInput("encryptedId:")
			DeleteUser(conn, encryptedId)
		case "exit":
			break L
		}
	}
}
```

`go run client/main.go`などのコマンドで実行できます。
次のように対話形式でAPIを実行出来ると思います。
(データは内部変数でしか保持していないため、サーバを終了するとリセットされます)
<img width="572" alt="スクリーンショット 2019-03-20 2.49.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/274674/56fa8fa2-0312-eeea-4de0-61b7de53b36c.png">

## REST API対応
こちらは表題の[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway)を利用すれば簡単に出来ました。

```golang:gateway/main.go
package main

import (
	"flag"
	"net/http"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	pb "grpc-gateway/proto"
)

var (
	userServiceEndpoint = flag.String("user_service_endpoint", "localhost:50000", "Users sample")
)

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

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, *userServiceEndpoint, opts)
	if err != nil {
		return err
	}

	return http.ListenAndServe(":50001", mux)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := run(); err != nil {
		glog.Fatal(err)
	}
}
```

実装はREADMEの「Write an entrypoint」を数行変更するくらいで、むしろgRPCのサーバとクライアントの実装が余程時間がかかりました。

```diff:サンプルの変更箇所
(省略)

// 各自のprotoを参照するように変更
-  gw "path/to/your_service_package"
+  pb "grpc-gateway/proto"

(省略)

var (
// ポートと名前を変更
-  echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
+  userServiceEndpoint = flag.String("user_service_endpoint", "localhost:50000", "Users sample")
)

(省略)

  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithInsecure()}
// protobufから生成したサービスに変更
-  err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
+  err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, *userServiceEndpoint, opts)
  if err != nil {
    return err
  }
```

`go run gateway/main.go`などのコマンドで実行できます。
serverとgatewayが実行されている状態でREAT APIを実行できます。
テストクライアントは[insomnia](https://insomnia.rest)を利用しました。

- 新規ユーザの追加
<img width="945" alt="スクリーンショット 2019-03-20 3.08.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/274674/ed79ece0-6d09-416d-b5e6-96c9b9c5f947.png">

- ユーザ一覧取得
<img width="952" alt="スクリーンショット 2019-03-20 3.08.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/274674/9920893d-84b0-b2f6-0b28-f95f45732439.png">

## 終わりに
今回はgrpc-gatewayを用いたREST対応について説明しました。
検証の意味合いが強く、説明が不十分な箇所は多々あると思います。
需要があれば、次回以降でgRPCの最初から詳しく調査して記事にしていこうと思います。

### Wantedlyでもブログ投稿してます
Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers