Docker x Go x gRPCなサーバーを立てようじゃないの

## 前提とか
- goの実行環境
- dockerの実行環境
- mac os

## まずはGoを使ってgRPCサーバを立ててみる
##1. 必要なものをinstall
gRPCxGoは色々なところで書かれているので簡単に。

### GoのgRPCライブラリをinstall
```
go get -u google.golang.org/grpc
```
### Protocol Buffersをインストール
- IDL(インターフェイス定義言語)で書かれたファイルをコンパイルするのに必要。

```
brew install protobuf
```

## Protocol Buffersのプラグインをインストール
- IDLに書かれた定義をGo言語へ変換するのに必要。インターフェイスをGo言語で自動生成する。

```
go get github.com/golang/protobuf/protoc-gen-go
```
protoc-gen-goはPATHが通った場所に置く必要があるので、`.bash_profile`やらに、以下を記述してPATHを通してあげる。自分は`$GOPATH/bin`をPATHに通しているので`protoc-gen-go`の実行ファイルをそこに配置した。

```
export PATH="$GOPATH/bin:$PATH"
type protoc-gen-go -> "protoc-gen-go is ~/go/bin/protoc-gen-go"
```

## ※gRPCミドルウェアのインストール
dockerで動かしている時に、通信が確立されているのかを確認するため、サーバ側のアクセスログを取得したかった。なのでそれらのミドルウェアをインストールする。

```
go get -u github.com/grpc-ecosystem/go-grpc-middleware
go get -u github.com/sirupsen/logrus
```

## 2. サーバの実装
ディレクトリ構成は以下のようにする想定。go.modは空でいいので作っておく。

```
server
|
 ---pb--increment.pb.go
|    
 ---proto--increment.proto
|
 ---service--increment.go
|
 ---main.go
 ---go.mod

```

最初にIDLを書いてインターフェイスを定義する。

```protobuf:increment.proto

syntax = "proto3";
package increment;

message IncrementRequest {
    int32 number = 1;
}

message IncrementResponse {
    int32 number = 1;
}

service IncrementService {
    rpc GetAndIncrement (IncrementRequest)
        returns (IncrementResponse);
}
```
Goのインターフェイスを生成する。

```bash:console
  # プロジェクトのトップにいることを確認
$ pwd #~/go/src/github.com/TakeruTakeru/server
  # protoをコンパイルしてGoのインターフェイスを作成
$ protoc  -I ./proto --go_out=plugins=grpc:./pb increment.proto
```
すると`pb`配下に`increment.pb.go`が作られているので確認する。
生成されたインターフェイスの実装を行う。
`service`配下に`increment.go`を作成し、以下のように実装する。

```go:increment.go
package service

import (
	"context"
	pb "github.com/TakeruTakeru/gserver/pb"
)

type IncrementService struct {
	cacheNum int32
}

func (s *IncrementService) Increment(ctx context.Context, req *pb.IncrementRequest) (*pb.IncrementResponse, error) {
	n := req.GetNumber() + 1
	return &pb.IncrementResponse{Number: n}, nil
}

func (s *IncrementService) GetAndIncrement(ctx context.Context, req *pb.IncrementRequest) (*pb.IncrementResponse, error) {
	if s.cacheNum != 0 {
		s.cacheNum = s.cacheNum + 1
	} else {
		s.cacheNum = req.GetNumber()
	}
	return &pb.IncrementResponse{Number: s.cacheNum}, nil
}

func NewIncrementService() *IncrementService {
	return &IncrementService{}
}
```
サービスロジックに関わる部分は以上の手続きで十分である。
今度はサーバの実装をする。
ミドルウェアを適用しない場合は[こちら](https://webbibouroku.com/Blog/Article/grpc-sample)を参考にして実装してください。
また、`net.Listen`するさいに、第二引数はポート指定のみにしてください*IPを指定すると、そのIPのみをlistenするので、例えばループバックアドレスを指定しているとIPからの接続のみ受け付けるので、開発環境ではlocalhostから繋いでたから上手くいってたのに、dockerでポートフォワードしてるのに繋がらないみたいなことになる。

```golang:main.go
package main

import (
	"log"
	"net"
	"os"
	"time"

	pb "github.com/TakeruTakeru/gserver/pb"
	service "github.com/TakeruTakeru/gserver/service"
	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
	grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
)

func main() {
	listen, err := net.Listen("tcp", ":5555")
	if err != nil {
		log.Fatalln(err)
	}

	logrus.SetLevel(logrus.DebugLevel)
	logrus.SetOutput(os.Stdout)
	logrus.SetFormatter(&logrus.JSONFormatter{})
	logger := logrus.WithFields(logrus.Fields{})

	opts := []grpc_logrus.Option{
		grpc_logrus.WithDurationField(func(duration time.Duration) (key string, value interface{}) {
			return "grpc.time_ns", duration.Nanoseconds()
		}),
	}

	grpc_logrus.ReplaceGrpcLogger(logger)

	server := grpc.NewServer(
		grpc_middleware.WithUnaryServerChain(
			grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
			grpc_logrus.UnaryServerInterceptor(logger, opts...),
		),
	)
	service := service.NewIncrementService()

	pb.RegisterIncrementServiceServer(server, service)

	if err := server.Serve(listen); err != nil {
		panic(err)
	}
}
```
## 3. gRPC Clientの実装
次にクライアントの実装をぱぱっと作る。

```go:client.go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	pb "github.com/TakeruTakeru/gserver/pb"
	"google.golang.org/grpc"
)

func main() {
	connection, err := grpc.Dial("localhost:5555", grpc.WithInsecure())
	if err != nil {
		log.Fatalln("did not connect: %s", err)
	}
	defer connection.Close()

	client := pb.NewIncrementServiceClient(connection)

	context, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	var number int32 = 0

	response, err := client.GetAndIncrement(context, &pb.IncrementRequest{Number: number})
	if err != nil {
		log.Println(err)
	}

	fmt.Println(response.GetNumber())
	response1, err := client.GetAndIncrement(context, &pb.IncrementRequest{Number: number})
	if err != nil {
		log.Println(err)
	}

	fmt.Println(response1.GetNumber())
	response2, err := client.GetAndIncrement(context, &pb.IncrementRequest{Number: number})
	if err != nil {
		log.Println(err)
	}

	fmt.Println(response2.GetNumber()) // 1001
}
```
これで一通り必要なものができたので、実際に`go run`して動作を確認する。

```bash:console
ls # main.go pb proto service go.mod
go run main.go
```
プロセスが立ち上がり、ポート5555でtcp通信を受け付けるようになったので、先ほど作った`client.go`も`go run`する。

```bash:console
go run client.go # 1, 2, 3
```

これでローカル環境でgRPCの動作確認が完了した。
今回はHeroku上でdocker環境のアプリケーションを動かしたいので、dockerイメージにビルドしたファイルも含めるようにする。(ここら辺のことはあまり調べきってないので別のやり方があるのなら教えていただけると助かります)

## Docker build
まず最初にinstallしたgrpcなどの依存モジュールをまとめる。
以下のコマンドを実行すると`go.mod`に依存モジュールの情報がまとめられる。

```bash:console
ls # main.go pb proto service go.mod
vgo list
```
今回のdockerイメージは以下のように定義する。
main.goのディレクトリを全てコピーし、依存関係を解決してくれるようにvgoをインストールしてビルドする流れ。

```dockerfile:Dockerfile
FROM golang:latest as gobase

ENV PATH=$PATH:$GOPATH/bin
ENV GOARCH="amd64"
ENV GOOS="linux"
WORKDIR /go/src/github.com/TakeruTakeru
COPY . .
RUN cd ../; go get -u golang.org/x/vgo
RUN go build -o $GOPATH/bin/grpc_server
```
`docker build. -t grpc_exeample `すると、、、

```
Step 8/8 : RUN go build -o $GOPATH/bin main.go
 ---> Running in 6109eb2d49aa
go: downloading github.com/sirupsen/logrus v1.4.2
go: downloading google.golang.org/grpc v1.24.0
go: downloading github.com/golang/protobuf v1.3.2
go: downloading github.com/grpc-ecosystem/go-grpc-middleware v1.1.0
go: extracting github.com/sirupsen/logrus v1.4.2
go: downloading golang.org/x/sys v0.0.0-20190422165155-953cdadca894
go: extracting github.com/grpc-ecosystem/go-grpc-middleware v1.1.0
go: extracting github.com/golang/protobuf v1.3.2
go: extracting google.golang.org/grpc v1.24.0
go: downloading google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
go: downloading golang.org/x/net v0.0.0-20190311183353-d8887717615a
go: extracting golang.org/x/sys v0.0.0-20190422165155-953cdadca894
go: extracting golang.org/x/net v0.0.0-20190311183353-d8887717615a
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
go: extracting google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
go: finding github.com/golang/protobuf v1.3.2
go: finding google.golang.org/grpc v1.24.0
go: finding golang.org/x/net v0.0.0-20190311183353-d8887717615a
go: finding google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
go: finding golang.org/x/sys v0.0.0-20190422165155-953cdadca894
go: finding golang.org/x/text v0.3.0
go: finding github.com/grpc-ecosystem/go-grpc-middleware v1.1.0
go: finding github.com/sirupsen/logrus v1.4.2
```
勝手にモジュールを落としてきてくれていることがわかる。

## Docker run & 動作確認
実際に`docker run`してみる。この時、ローカルでサーバを立ち上げているならば忘れずにkillしておく。docker containerのポートフォワーディングと被り、コンテナがエラーを起こしてしまうためだ。

```bash:console
docker run --name grpc_exeample -p 5555:5555 -it grpc_exeample bash
```
コンテナ内に入り、ビルドできていれば`/go/bin/grpc_server`というバイナリがあるので、実際に動かす(`./grpc_server`)。

別のシェルプロセスを立ち上げて先ほどの`client.go`を実行する。
下記のレスポンスがサーバ、クライアント共に出力されていれば完了！
お疲れ様でした！

```text:server
root@6962b46dce49:/go/bin# ./grpc_server 
{"grpc.code":"OK","grpc.method":"GetAndIncrement","grpc.request.deadline":"2019-11-04T02:48:37Z","grpc.service":"increment.IncrementService","grpc.start_time":"2019-11-04T02:48:36Z","grpc.time_ns":167200,"level":"info","msg":"finished unary call with code OK","peer.address":"172.17.0.1:56182","span.kind":"server","system":"grpc","time":"2019-11-04T02:48:36Z"}
{"grpc.code":"OK","grpc.method":"GetAndIncrement","grpc.request.deadline":"2019-11-04T02:48:37Z","grpc.service":"increment.IncrementService","grpc.start_time":"2019-11-04T02:48:36Z","grpc.time_ns":55600,"level":"info","msg":"finished unary call with code OK","peer.address":"172.17.0.1:56182","span.kind":"server","system":"grpc","time":"2019-11-04T02:48:36Z"}
{"grpc.code":"OK","grpc.method":"GetAndIncrement","grpc.request.deadline":"2019-11-04T02:48:37Z","grpc.service":"increment.IncrementService","grpc.start_time":"2019-11-04T02:48:36Z","grpc.time_ns":48800,"level":"info","msg":"finished unary call with code OK","peer.address":"172.17.0.1:56182","span.kind":"server","system":"grpc","time":"2019-11-04T02:48:36Z"}
{"level":"info","msg":"transport: loopyWriter.run returning. connection error: desc = \"transport is closing\"","system":"system","time":"2019-11-04T02:48:36Z"}
```

```text:client
1
2
3
```

## 所感
gRPCに関する情報は結構転がっているので割りかし困ることはなかった。
覚えることがたくさんあるが、それにしてもリターンが沢山あるのでgRPCもっと知りたいなと。

***
参考にさせていただいたページ
[go-grpc-middlewareを一通り試してみる](https://qiita.com/Morix1500/items/7a20d76a931af68d860)
[Goで書いたサーバーをHerokuにDocker Deployする](https://qiita.com/croquette0212/items/2b85aa2c6b2933244f07)
[gRPC に Go言語 で入門する方法(環境構築から通信まで)](https://webbibouroku.com/Blog/Article/grpc-sample)