gRPC/GoのServerをGKEにデプロイする
はじめに
今回はGoogle Kubernetes Engine(GKE)を利用してgRPC通信の動作検証をしました。
本記事ではGoを使用したgRPC通信の実装例と、GKEへのデプロイ手順を記載しています。
構成図は以下のようになります。LoadBalancer(LB)でgRPCリクエストを受け付けてPodにリクエストをしています。Pod内では BookService
が UserService
にgRPCリクエストをして、得られた情報をもとにレスポンスを返しています。
- はじめに
- 検証に使用したプログラム
- user_service/main.go の実装
- book_service/main.go の実装
- Dockerイメージの作成とGoogle Cloud Registry(GCR)への登録
- サーバーを動かすためのGKEクラスタの作成
- ローカルからGKEクラスタにgRPCリクエストをする
- まとめ
- 参考文献
検証に使用したプログラム
今回使用したプログラムは、前回の記事で作成した y-zumi/grpc-go に機能を追加したものです。
パッケージ構成は以下のようになっており、 book_service
と user_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
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-service
とbook-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-service
のEXTERNAL_IP
にgrpcurlを使って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は少ししか触れていませんが、様々な設定をすることができるので、今後の記事で試してみようと思います。