gRPC/GoのServerをGKEにデプロイする

はじめに

今回はGoogle Kubernetes Engine(GKE)を利用してgRPC通信の動作検証をしました。 本記事ではGoを使用したgRPC通信の実装例と、GKEへのデプロイ手順を記載しています。
構成図は以下のようになります。LoadBalancer(LB)でgRPCリクエストを受け付けてPodにリクエストをしています。Pod内では BookServiceUserService にgRPCリクエストをして、得られた情報をもとにレスポンスを返しています。

f:id:y-zumi:20190913233920j:plain
システム構成図

検証に使用したプログラム

今回使用したプログラムは、前回の記事で作成した y-zumi/grpc-go に機能を追加したものです。
パッケージ構成は以下のようになっており、 book_serviceuser_service の2つのサービスと、それぞれに対応するDockerfileとprotoファイルがあります。 deployment.yaml は各サービスをGKEへデプロイする際に使用します。

├── book_service
│   └── main.go
├── user_service
│   └── main.go
├── docker
│   ├── book
│   │   └── Dockerfile
│   └── user
│       └── Dockerfile
├── proto
│   ├── book
│   │   ├── book.pb.go
│   │   └── book.proto
│   └── user
│       ├── user.pb.go
│       └── user.proto
└── deployment.yaml

github.com

user_service/main.go の実装

UserServiceの実装は以下のようになります。

  • UserServiceの定義
    • FindByIDメソッドの定義
  • main関数の定義
    • gRPC Serverの起動
    • gRPC ServerにUserServiceを登録する

UserServiceの定義

proto/user/user.proto をもとにUserServiceの構造体とメソッドを定義しています。UserServiceにはFindByIDというRemoteProcedureCall(RPC)を定義しています。FindByIDは「UserのidをもとにUserの情報を返す」RPCです。

service Users {
    // Find user by user id
    rpc FindByID (FindByIDRequest) returns (FindByIDResponse) {}
}

// Find user information
message FindByIDRequest {
    string id = 1;
}

// Return information of found user
message FindByIDResponse {
    User user = 1;
}

// A User resource
message User {
    // user's id
    string id = 1;

    // user's nickname
    string name = 2;
}

main関数の定義

main関数の中ではgRPC Serverの起動と、UserServiceをgRPC Serverに登録する処理を実装しています。

func main() {
    // Start listening port
    lis, err := net.Listen("tcp", ":50001")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // Register UsersServer to gRPC Server
    s := grpc.NewServer()
    user.RegisterUsersServer(s, &UserService{})

    // Add grpc.reflection.v1alpha.ServerReflection
    reflection.Register(s)

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

book_service/main.go の実装

BookServiceの実装はUserServiceと同様に以下のようになります。

  • BookServiceの定義
    • FindLendingBookByIDメソッドの定義
  • main関数の定義
    • gRPC Serverの起動
    • gRPC ServerにBookServiceを登録する

BookServiceの定義

proto/book/book.proto をもとにBookServiceの構造体とメソッドを定義しています。BookServiceにはFindLendingBookByIDという「任意の本の貸出情報を取得し返す」RPCを定義しています。本の貸出情報はFindLendingBookByIDResponseに定義されており、BookとUserの情報が含まれています。

service Books {
    rpc FindLendingBookByID (FindLendingBookByIDRequest) returns (FindLendingBookByIDResponse);
}

message FindLendingBookByIDResponse {
    Book book = 1;
    user.User borrower = 2;
}

message Book {
    string id = 1;
    string title = 2;
    string status = 3;
}

FindLendingBookByIDResponseにはUserの情報が含まれていますが、BookServiceはUserの情報を持っていないためUserServiceに問い合わせる必要があります。そのため、BookServiceはFindLendingBookByID内で、UserServiceへgRPCリクエストをしてUserの情報を取得しています。

type BookService struct {
    client user.UsersClient
}

func (s *BookService) FindLendingBookByID(ctx context.Context, req *book.FindLendingBookByIDRequest) (*book.FindLendingBookByIDResponse, error) {
    // request UserService
  findByIDRequest := user.FindByIDRequest{
        Id: faker.UUIDDigit(),
    }
    borrower, err := s.client.FindByID(ctx, &findByIDRequest)
    if err != nil {
        return nil, errors.New("user is not found error")
    }

    return &book.FindLendingBookByIDResponse{
        Book: &book.Book{
            Id:     req.Id,
            Title:  faker.Word(),
            Status: BookStatusLending,
        },
        Borrower: borrower.User,
    }, nil
}

main関数の定義

gRPC Serverの起動は同じ実装ですが、BookServiceはUserServiceにアクセスする必要があるため、UserServiceにアクセスするためのUserClientの実装もmain関数内で行っています。

func main() {
    // Start listening port
    // 省略

    // Register BookService to gRPC Server
    s := grpc.NewServer()
    bookService, err := createBookService()
    if err != nil {
        log.Fatalf("did not create book service: %v", err)
    }
    book.RegisterBooksServer(s, bookService)

    // Start server
    // 省略
}

func createBookService() (*BookService, error) {
    cli, err := newUserClient()
    if err != nil {
        return nil, errors.Wrap(err, "did not create user client")
    }

    return NewBookService(cli), nil
}

func NewBookService(client user.UsersClient) *BookService {
    return &BookService{
        client: client,
    }
}

func newUserClient() (user.UsersClient, error) {
    conn, err := grpc.Dial("localhost:50001", grpc.WithInsecure())
    if err != nil {
        return nil, errors.Wrap(err, "did not connect localhost:5001")
    }

    return user.NewUsersClient(conn), nil
}

Dockerイメージの作成とGoogle Cloud Registry(GCR)への登録

docker/ディレクトリ配下にUserServiceとBookServiceのDockerfileがあります。ファイルの中身は以下のようになっています

FROM golang:1.13.0 AS builder
WORKDIR /go/src/github.com/y-zumi/grpc-go
COPY . .
RUN make build-user

FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /go/src/github.com/y-zumi/grpc-go/bin/user /usr/local/bin
ENTRYPOINT ["/usr/local/bin/user"]

imageの作成は以下のコマンドで行います。今回はDockerイメージをGCRに登録するためイメージの名前をgcr.io/[YOUR_PROJECT_ID]/user-service:v1.0のようにする必要があります。
[YOUR_PROJCET_ID]にはご自身のGoogle Cloud Platform ConsoleのプロジェクトIDを記載してください。

> docker build --tag gcr.io/[YOUR_PROJECT_ID]/user-service:v1.0 -f docker/user/Dockerfile .
Sending build context to Docker daemon
...

> docker build --tag gcr.io/[YOUR_PROJECT_ID]/book-service:v1.0 -f docker/book/Dockerfile .
Sending build context to Docker daemon
...

GCRへのDockerイメージの登録は以下のコマンドで行います。
登録後はgcloud container image listで無事に登録できているか確かめることができます。

> gcloud auth configure-docker
> docker push gcr.io/[YOUR_PROJECT_ID]/user-service:v1.0
The push refers to repository [gcr.io/[YOUR_PROJECT_ID]/user-service]
...

> docker push gcr.io/[YOUR_PROJECT_ID]/book-service:v1.0
The push refers to repository [gcr.io/[YOUR_PROJECT_ID]/book-service]
...

> gcloud container images list
NAME
gcr.io/[YOUR_PROJECT_ID]/book-service
gcr.io/[YOUR_PROJECT_ID]/user-service

サーバーを動かすためのGKEクラスタの作成

次はGKEクラスタを作成し、UserServiceとBookServiceをGKEクラスタにデプロイします。

クラスタの作成は以下のコマンドで行います。

> gcloud container clusters create grpc-go-cluster --zone asia-northeast1 --num-node 1
> gcloud container clusters list
NAME             LOCATION         MASTER_VERSION  MASTER_IP       MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS
grpc-go-cluster  asia-northeast1  1.13.7-gke.8    35.243.122.104  n1-standard-1  1.13.7-gke.8  3          RUNNING

DeploymentとServiceはdeployment.yamlにまとめて実装しています。
Deploymentをみると、1つのPodでuser-servicebook-serviceの2つのコンテナが起動するようになっています。Serviceでは、ポート80でアクセスを受け付けて、ポート50011(book-service)に転送しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpc-go-deployment
  labels:
    app: grpc-go
spec:
  replicas: 3
  selector:
    matchLabels:
      app: grpc-go
  template:
    metadata:
      labels:
        app: grpc-go
    spec:
      containers:
        - name: user-service
          image: gcr.io/kouzoh-p-y-zumi/user-service:v1.0
          ports:
            - containerPort: 50001
        - name: book-service
          image: gcr.io/kouzoh-p-y-zumi/book-service:v1.0
          ports:
            - containerPort: 50011
---
apiVersion: v1
kind: Service
metadata:
  name: grpc-go-service
spec:
  type: LoadBalancer
  selector:
    app: grpc-go
  ports:
    - port: 80
      targetPort: 50011
      protocol: TCP

以下のコマンドでGKEクラスタにDeploymentなどを作成し、BookServiceとUserServiceを起動させます。
実際にPodが動いていることが確認できれば大丈夫です。

> kubectl apply -f deployment.yaml
deployment.apps/grpc-go-deployment created
service/grpc-go-service created

> kubectl get pods
NAME                                  READY   STATUS    RESTARTS   AGE
grpc-go-deployment-57bbc76bd4-js9xf   2/2     Running   0          46s
grpc-go-deployment-57bbc76bd4-nlkkg   2/2     Running   0          46s
grpc-go-deployment-57bbc76bd4-tw84j   2/2     Running   0          46s

ローカルからGKEクラスタにgRPCリクエストをする

grpc-go-serviceEXTERNAL_IPgrpcurlを使ってgRPCリクエストをします。
レスポンスが返ってきていることが確認できたらgRPC通信が成功しています。

> kubectl get services
NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
grpc-go-service   LoadBalancer   10.47.242.103   34.84.72.59   80:32415/TCP   4h48m
kubernetes        ClusterIP      10.47.240.1     <none>        443/TCP        5h45m

> grpcurl ls -k 34.84.72.59:80
book.Books
grpc.reflection.v1alpha.ServerReflection

> echo '{"id": "12345"}' | grpcurl -k call 34.84.72.59:80 book.Books.FindLendingBookByID | jq .
{
  "book": {
    "id": "12345",
    "title": "eligendi",
    "status": "Lending"
  },
  "borrower": {
    "id": "80546886febf4166b236df1214afa559",
    "name": "Prof. Yasmeen Casper"
  }
}

まとめ

今回はGKEクラスタにgRPC Serverをデプロイして動作確認を行いました。Kubernetesは少ししか触れていませんが、様々な設定をすることができるので、今後の記事で試してみようと思います。

参考文献

cloud.google.com

cloud.google.com

kubernetes.io