LoginSignup
4
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-03

前提とか

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

increment.proto

syntax = "proto3";
package increment;

message IncrementRequest {
    int32 number = 1;
}

message IncrementResponse {
    int32 number = 1;
}

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

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

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を作成し、以下のように実装する。

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でポートフォワードしてるのに繋がらないみたいなことになる。

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

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

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

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

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

console
go run client.go # 1, 2, 3

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

Docker build

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

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

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

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

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

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

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

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

所感

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


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

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5