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

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 を書いてインターフェイスを定義する。

syntax = "proto3";
package increment;

message IncrementRequest {
    int32 number = 1;
}

message IncrementResponse {
    int32 number = 1;
}

service IncrementService {
    rpc GetAndIncrement (IncrementRequest)
        returns (IncrementResponse);
}

Go のインターフェイスを生成する。

  # プロジェクトのトップにいることを確認
$ 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を作成し、以下のように実装する。

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{}
}

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

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 の実装

次にクライアントの実装をぱぱっと作る。

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して動作を確認する。

ls # main.go pb proto service go.mod
go run main.go

プロセスが立ち上がり、ポート 5555 で tcp 通信を受け付けるようになったので、先ほど作ったclient.gogo runする。

go run client.go # 1, 2, 3

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

Docker build

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

ls # main.go pb proto service go.mod
vgo list

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

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 のポートフォワーディングと被り、コンテナがエラーを起こしてしまうためだ。

docker run --name grpc_exeample -p 5555:5555 -it grpc_exeample bash

コンテナ内に入り、ビルドできていれば/go/bin/grpc_serverというバイナリがあるので、実際に動かす(./grpc_server)。

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

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"}
1
2
3

所感

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


参考にさせていただいたページ go-grpc-middleware を一通り試してみる Go で書いたサーバーを Heroku に Docker Deploy する gRPC に Go 言語 で入門する方法(環境構築から通信まで)