gRPC+Go を触ってみたので、ざっくり纏めてみた [ハンズオンつき]

## はじめに
最近、社内でマイクロサービスが話題に上がるにつれ、
gRPCという言葉をよく聞くようになりました。

前から興味があったことと、今後業務でgRPCで使う可能性がありそうとのことだったので、
概要と雰囲気を掴むために記事にまとめてみました。

## 記事の読み進め方
記事の内容を下2つに分けてgRPCを理解を深めていきます :eyes:
* [gRPCの概要](https://qiita.com/mnoji/items/20895275d923f02821fd#grpc%E3%81%A3%E3%81%A6%E3%81%AA%E3%81%AB)
* [簡単なハンズオン](https://qiita.com/mnoji/items/20895275d923f02821fd#%E3%83%8F%E3%83%B3%E3%82%BA%E3%82%AA%E3%83%B3)

## gRPCってなに？
Google が開発した RPC(Remote Procedure Call)システムです。

特徴をざっくりとまとめますと、以下のようなもの
* HTTP/2 通信をベースに高速かつ効率的な通信が可能
* Protocol Buffers と呼ばれるIDLを利用して、クライアントとサーバーの通信で必要なソースコードを自動作成してくれる
* クライアントとサーバで異なる言語を利用可能 


![gRPC_Image_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617507/1c33e56d-c365-914c-0b98-ce5026b11c78.png)

(※画像はgRPC公式より抜粋)


余談ですが、gRPCのgはバージョンによって意味が違うらしいです (googleのgではなかったのか・・・)
https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md

### そもそも、RPCとはなんぞや？
ローカルなどの環境から他サーバー上の関数を呼び出して、実行するようとするような仕組みです。
これだけでは分かりづらいので、RESTとRPCを整理して比較して見てみましょう。

* REST
設計思想としては、リソース(オブジェクト)を中心に考え、これに対して HTTP メソッドで操作していく。
(特定のリソースに対してCURD操作を行うのが前提) <br>
シンプルでスケーラブルな API を作ることが前提にあり、
REST では原則を踏まえた上で、仕様を決めて実装を行います。


* RPC
メソッドの呼び出しが基点であり、結果としてレスポンスとして返される形。
データが副産物という立場であるため、RESTの設計思想としては大きく異なります。<br>
また、RESTと比較して制約が緩いため、比較的な柔軟なシステム実装が可能。

なるほど、なるほど・・・
それぞれ、設計の思想は異なっていて、用途よって使い分ける感じでしょうか

実際にはRESTで対応しきれない部分をRPCで作成する感じになりそうですね :rolling_eyes:


## gRPCの4つの通信方式とユースケース
gRPCは4つの通信方式があり用途別に使い分けることが出来ます。
(※いづれもProtocol Buffersを利用して、自動的に作成されるコードを用いて実装が可能です！)

#### Unary RPC
1つのリクエストに対して１つのレスポンスを返す一般的な通信方式。
(ex. サーバ間通信、APIなど)

#### Server streaming RPC
クライアントから送られてきた一つのリクエストに対して、複数回に分けてレスポンスを返す方式。
(ex. 任意のタイミングでの通知。push通知など)

#### Client streaming RPC
クライアントからリクエストを分割して送信。
サーバーはすべてのリクエストを受け取ってからレスポンスを返す方式。
(ex. クライアントから大きいデータが送信する必要がある場合)

#### Bidirectional streaming RPC
クライアントからリクエストが送られてきた際に、サーバーとクライアントはコネクションを確立。
任意のタイミングでレスポンスを取り合う方式。
(ex. オンライン対戦やチャットなど)

## gRPCのメリット・デメリットについて
#### メリット
* 通信はやい。効率的。
  - HTTP/2通信対応
  - ヘッダの圧縮
  - デフォルトでバイナリ通信
  - リクエスト/レスポンスを並列処理が可能
  - リクエストの優先順位付が可能

* 通信部分のコード生成が簡単 (自動でできる)
* 特定言語に依存しない

#### デメリット
* 基本的にはHTTP/2のみしか対応していない (※一部除く)
* それぞれの言語をビルドするのが若干手間・・・
* RESTのようなパスによる親子構造などはできない
* 動作確認時に、どうやってRPCメソッドを呼び出すのか検討する必要がある (専用のテストコード書く　or ツールを導入)
* 負荷検証の際に、メジャーなツールがデフォルトではgRPCに対応してなかったりする (ex. jmeter や locust)
* gRPCをwebでも利用できるが、ブラウザ上の制限などいろいろ制約があったりする
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md

## ハンズオン
gRPCの雰囲気をつかむためにも簡単なハンズオンを用意しました
(※gRPC の [getting start](https://grpc.io/docs/languages/go/quickstart/) と AWS の [prescriptive-guidance](https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/patterns/deploy-a-grpc-based-application-on-an-amazon-eks-cluster-and-access-it-with-an-application-load-balancer.html) を参考にしてます)

以下の2つのハンズオンを用意してます
* [サンプル１(EC2 to EC2)](https://qiita.com/mnoji/items/20895275d923f02821fd#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%EF%BC%91ec2-to-ec2)
* [サンプル２ (ローカルPCからEKSのポッドへ)](https://qiita.com/mnoji/items/20895275d923f02821fd#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%EF%BC%92-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%ABpc%E3%81%8B%E3%82%89eks%E3%81%AE%E3%83%9D%E3%83%83%E3%83%89%E3%81%B8)

### サンプル１(EC2 to EC2)
![qiita-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/719015/cba149b8-09ba-0df5-1229-ca538aac3a98.png)

#### やること
EC2上にクライアント・サーバ用のコードを配置して、gRPCの通信を行います
(※ベースは[grpcのサンプルコード](https://github.com/grpc/grpc-go)で変更を加えたもの)

#### 前提情報
* Goが実行できるEC2を用意しておくこと(作業用環境 + Clinet +  Server)
* SG(TCP:50051)に疎通用の穴をあけておくこと

#### 環境面の補足
* 作業環境について
  - amazon linux 2
  - Go v1.17.8

* 作業環境にインストールする設定
  - libprotoc v3.20.0
  - protoc-gen-go-grpc v1.1
  - protoc-gen-go v1.26

* クライアントとサーバの環境について
  - amazon linux 2
  - Go v1.17.8

#### 出来上がるディレクトリパスの構成
```
$ tree
grpc_demo
├── example_01
│   ├── client
│   │   ├── go.mod
│   │   ├── go.sum
│   │   └── main.go
│   └── server
│       ├── go.mod
│       ├── go.sum
│       └── main.go
└── proto
    ├── echo.pb.go
    ├── echo.proto
    └── echo_grpc.pb.go
```


##### 1. 環境の用意と設定ファイルの作成

* ファイルを自動生成させるために、Protocol Buffersをインストールする
https://github.com/protocolbuffers/protobuf/releases
```shell
$ curl -OL https://github.com/google/protobuf/releases/download/v3.20.0/protoc-3.20.0-linux-x86_64.zip
$ unzip protoc-3.20.0-linux-x86_64.zip -d protoc
$ sudo mv protoc/bin/* /usr/local/bin/
$ sudo mv protoc/include/* /usr/local/include/

# ちゃんとインストール・パスが通っているか確認
$ protoc --version
libprotoc 3.20.0
```

* Goのプロトコルコンパイラ用のプラグインをインストール。パスを通しておく
```
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
$ export PATH="$PATH:$(go env GOPATH)/bin"
```

* protoファイルを用意する
```protobuf:proto/echo.proto
syntax = "proto3";

option go_package = "<XXXXXXX/grpc_demo/proto>"; // GITで管理

package echo;

service grpc_echo_test {
  rpc hello_msg (EchoRequest) returns (EchoResponse) {}
}

message EchoRequest {
  string req_message = 1;
}

message EchoResponse {
  string res_message = 1;
}
```

* 用意したprotc ファイルを元に、gRPC通信を利用するためのファイルを生成
```shell
$ protoc --go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative  proto/echo.proto

$ ll ./proto
-rw-rw-r-- 1 web web 6323  3月 25 11:15 echo.pb.go       # 作成されたコード(メッセージやシリアライズ)
-rw-rw-r-- 1 web web  225  3月 24 11:00 echo.proto       # 元のコード
-rw-rw-r-- 1 web web 3317  3月 25 11:15 echo_grpc.pb.go  # 作成されたコード(gRPCのサーバ/クライアント)

# 作成したコードはGIT経由で参照させるため、それぞれの個人カウントでコードをプッシュ & go install しておく
go install XXXXXXX/grpc_demo/proto
```


* gRPCの通信を行うためのクライアント・サーバーファイルを用意
```golang:example_01/server/main.go
package main

import (
        "context"
        "flag"
        "log"
        "time"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        pb "XXXXXXX/grpc_demo/proto"     // 先ほど作成したコード作成したファイルをGITから参照
)

type server struct {
	pb.UnimplementedGrpcEchoTestServer
}

var (
	port = flag.Int("port", 50051, "The server port")
)

func (s *server) HelloMsg(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
	log.Println("rceived:" + in.GetReqMessage())
	return &pb.EchoResponse{ResMessage: "Hello " + in.GetReqMessage()}, nil
}

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	reflection.Register(s)

	pb.RegisterGrpcEchoTestServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())

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


```golang:example_01/client/main.go
package main

import (
        "context"
        "flag"
        "log"
        "time"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        pb "XXXXXXX/grpc_demo/proto"     // 先ほど作成したコード作成したファイルをGITから参照
)

var (
        addr = flag.String("addr", "<サーバ側のprivate ip>:50051", "the address to connect to")
        name  = flag.String("msg", "world", "hello to name")
)

func main() {
        flag.Parse()
        conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))

        if err != nil {
                log.Fatalf("connection failed: %v", err)
        }
        defer conn.Close()
        c := pb.NewGrpcEchoTestClient(conn)
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)

        defer cancel()
        r, err := c.HelloMsg(ctx, &pb.EchoRequest{ReqMessage: *name})

        if err != nil {
                log.Fatalf("could not msg: %v", err)
        }

    log.Printf("rceived msg: %s ", r.GetResMessage())
}
```

##### 2. Cliet/Serverにファイルを配置して疎通確認
* 各サーバ(Client/Server)にファイルを配置。go module 周りのファイルを生成しておく
```
go mod init XXXX
go mod tidy
```

* gRPCによる通信を行い、処理が返ってくることを確認
```shell
# server 側での実施
$ cd example/server/
$ go run main.go
2022/03/29 15:11:37 server listening at [::]:50051
2022/03/29 15:14:02 rceived:world 
 
# clienet 側での実施
$ cd example/server/
$ go run main.go
2022/03/29 15:14:10 rceived msg: Hello world
```

※ 通信が行えていることが確認できたら、サンプル1のハンズオンはOK！

---

### サンプル２ (ローカルPCからEKSのポッドへ)
![qiita-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/719015/eeadc67c-cda4-f8ba-8457-cfb467cc01ee.png)

#### やること
ローカルPCからインターネットを経由して、EKSに配置したポッドと通信を行います
(※ サンプル１で利用したサーバ側のファイルはそのまま流用します)
 
#### 前提情報
  - EKSクラスターが用意されていること
  - kubectlがインストールされ、 EKS クラスターのリソースにアクセスするように設定されていること
  - AWS Load Balancer ControllerがAmazon EKS クラスター上でデプロイされていること
  - 利用可能なEcr/ACM/route53(ホストゾーン)が存在していること
  - 作業環境でDockerが利用できること

#### 環境面の補足
* 作業環境について
  - amazon linux 2
  - Docker v19.03.6-ce
  - aws cli v1.18.147

* サーバ側の環境について
  - eks (Kubernetes バージョン) v1.21
  - 利用するノードグループ: EC2(ASG)

* クライアント側の環境について
  - Ubuntu v20.04
  - Go v1.17.8
  - grpcurl v1.8.6 (※手順内で追加)

#### 出来上がるディレクトリパスの構成
```shell
$ tree
grpc_demo
├── example_01             # EC2 to EC2 のハンズオン用のファイル・ディレクトリ　
│   ├── client
│   │   ├── go.mod
│   │   ├── go.sum
│   │   └── main.go
│   └── server
│       ├── go.mod
│       ├── go.sum
│       └── main.go
├── example_02              # ローカルPC to EKS(Pods) のハンズオン用のファイル・ディレクトリ
│   ├── aws_resources
│   │   └── route53_record.json
│   ├── container
│   │   └── server
│   │       ├── Dockerfile
│   │       └── go        
│   │           ├── go.mod
│   │           ├── go.sum
│   │           └── main.go　# example1のファイルをそのまま流用
│   └── manifast
│       ├── deployment.yaml
│       └── ingress.yaml
└── proto                    # 共通で利用するファイル・ディレクトリ
    ├── echo.pb.go
    ├── echo.proto
    └── echo_grpc.pb.go

9 directories, 13 files
```
---

#### Server側の設定内容
##### 1. Go と Docekr image用のファイルの用意
* サンプルケース1で用意したproto ファイルとgoファイル(server)をそのまま流用して配置
```shell
$ mkdir -p example_02/container/server/go
$ cp example_01/server/main.go  example_02/container/server/go

# ディレクトリにもぐり、go moduleの管理するためのファイルを作成
$ example_02/container/server/go
$ go mod init XXXX
$ go mod tidy
```

* コンテナイメージ用のファイルを用意
```Dockerfile:example_02/container/server/Dockerfile
FROM golang:1.17

WORKDIR /app

RUN apt-get update && \
    apt-get -y install git unzip build-essential autoconf libtool

COPY go /app/grpc-server

EXPOSE 50051
ENTRYPOINT cd /app/grpc-server && go run /app/grpc-server/main.go
```

* ECRへイメージをプッシュ
```shell
# ログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

# イメージを作成して
docker build -t <タグ名> .
docker tag <タグ名>:latest <アカウント名>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>:latest

# ecrへプッシュ
docker push <アカウント名>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>:latest
```

##### 2.pod/albを展開するためのマニフェストファイルの用意

* Deplpyment と Service 用のファイルを用意
```yaml:manifast/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpcserver
  namespace: <任意のnamespace名>
spec:
  selector:
    matchLabels:
      app: grpcserver
  replicas: 1
  template:
    metadata:
      labels:
        app: grpcserver
    spec:
      containers:
      - name: grpc-demo
        image: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<ecrのリポジトリ名>:latest
        imagePullPolicy: Always
        ports:
        - name: grpc-server
          containerPort: 50051
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: "grpc-service-demo"
  namespace: <任意のnamespace名>
  annotations:
    service.alpha.kubernetes.io/app-protocols: '{"grpc":"HTTP2", "health": "HTTP2"}'
spec:
  type: NodePort
  ports:
    - port: 50051
      targetPort: 50051
      protocol: TCP
  selector:
    app: "grpcserver"
```

* ingress用の設定ファイルを用意
```yaml:manifast/ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig":
      { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    alb.ingress.kubernetes.io/backend-protocol-version: GRPC
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/subnets: subnet-XXXXXXX, subnet-XXXXXXX # 任意のサブネット
    alb.ingress.kubernetes.io/security-groups: sg-XXXXXXX, sg-XXXXXXX # 任意のSG (※443ポート開放済みのもの)
    alb.ingress.kubernetes.io/healthcheck-path: /
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:<アカウントID>:certificate/XXXXXX # 任意のACM
  labels:
    app: grpcserver
    environment: dev
  name: grpcserver
  namespace: <任意のnamespace名>
spec:
  rules:
  - host: <任意のホスト名>
    http:
      paths:
      - backend:
          serviceName: ssl-redirect
          servicePort: use-annotation
        path: /*
      - backend:
          serviceName: grpc-service-demo
          servicePort: 50051
```

* マニフェストファイルからPOD/ALBを作成する
```shell
# depolymentが作成されポッドが配置されることを確認
$ kubectl apply -f manifast/deployment.yaml
$ kubectl get deploument -n <任意のNamespace名>
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
grpcserver   1/1     0            1           4m

$ kubectl get pod -n <任意のNamespace名>
NAME                          READY   STATUS    RESTARTS   AGE
grpcserver-678dcdd5b8-kzh78   1/1     Running   0          5m1s

# albが作成されることを確認
$ kubectl apply -f manifast/ingress.yaml
$ kubectl get ingress -n <任意のNamespace名>
NAME         CLASS    HOSTS                     ADDRESS                                                                       PORTS   AGE
grpcserver   <none>   <任意のホスト名>   <alb名>.ap-northeast-1.elb.amazonaws.com   80      12m
```

##### 3.外部からドメインを利用してアクセスできるようにRoute53レコードを作成
* レコード登録のためにjsonファイルを用意
```json:route53_record.json
{
        "Comment": "Alias record for grpc",
        "Changes": [
                {
                        "Action": "CREATE",
                        "ResourceRecordSet": {
                                "Name": "<登録するドメイン>",
                                "Type": "A",
                                "AliasTarget": {
                                        "HostedZoneId": "<ELBのホストゾーンID>",
                                        "DNSName": "dualstack.<先ほど作成したALBのDNS>",
                                        "EvaluateTargetHealth": false
                                }
                        }
                }
        ]
}
```

* aws cli で route53にレコードを登録
```json:example_02/aws_resources/route53_record.json
$ aws route53 change-resource-record-sets --hosted-zone-id <Hostzone id(route53)> --change-batch file://route53_record.json --region=ap-northeast-1
{
    "ChangeInfo": {
        "Status": "PENDING",
        "Comment": "Alias record for grpc",
        "SubmittedAt": "2022-03-29T12:16:43.769Z",
        "Id": "/change/C082484523D9BRL0OXFSD"
    }
}
```

### Client側の設定内容と疎通確認
* grpcurlのインストール。パスも通しておく
```
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@v1.8.6
$ export PATH="$PATH:$(go env GOPATH)/bin"
```

* 外部からリクエストを飛ばし、想定したレスポンスが返ることを確認
```shell
# grpcサーバで利用しているサービス名を確認
$ grpcurl <登録したドメイン>:443 list
echo.grpc_echo_test
grpc.reflection.v1alpha.ServerReflection

# 中身のRPC名まで確認
$ grpcurl <登録したドメイン>:443 list echo.grpc_echo_test
echo.grpc_echo_test.hello_msg

# 上記で取得したRPCを指定して、リクエストを飛ばす
$ grpcurl -d '{"req_message": "hogehoge"}' <登録したドメイン>:443 echo.grpc_echo_test.hello_msg
{
  "resMessage": "Hello hogehoge"
}
```

※ サーバから上記のようなレスポンスが返ってきたら、サンプル2のハンズオンはOK！


## おわりに
今回 gRPCの雰囲気を掴むために、サンプルを用意しつつ、概要を追ってみました。

感覚としては、Protocol Buffersで生成されたファイルについて、ある程度の理解が必要そうですが、
中身を把握すればいろんな場面で応用出来そうな感じでした。
(動作確認の方法が少し面倒くさい形になりそう・・・)

今回で利用方法は把握出来たので、WEB 経由での gRPC の利用やHTTP/1 vs HTTPS/2 の性能比較なども、
今後記事などにしていきたいなと思いました :writing_hand_tone3:

## 参考
https://grpc.io/docs/languages/go/quickstart

https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/patterns/deploy-a-grpc-based-application-on-an-amazon-eks-cluster-and-access-it-with-an-application-load-balancer.html