istio-proxy で grpc-web する


## TL;DR :pencil: 

istio-proxy で grpc-web する場合は `EnvoyFilter` カスタムリソースを作って、 `envoy.grpc_web` を追加する。
[こんな感じで。](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web/-/blob/master/manifests/envoyfilter.yaml)

## はじめに

[gRPC](https://grpc.io/docs/)はHTTP2上で[protocol buffers](https://developers.google.com/protocol-buffers)を使用し、[Remote procedure call](https://en.wikipedia.org/wiki/Remote_procedure_call) する枠組みで、[grpc-web](https://grpc.io/blog/grpc-web-ga/)はブラウザ上からgRPCサーバに対してアクセスするための方法です。gRPCでWebAPIを開発するにあったって、grpc-webは（執筆時点では）無くてはならない存在です。

gRPC及び、grpc-webについては下記の記事がとってもわかり易いので興味のある方はご参照ください。

- [gRPCに入門する~gRPC-Webで実装もしてみる~](https://qiita.com/Le0tk0k/items/af2fbf1d246f0a788ec3)

さて、そんなgrpc-webでgRPCサーバにアクセスするためには、（再び執筆時点で） [special proxy](https://github.com/grpc/grpc-web#grpc-web) が必要で、 [Envoy](https://www.envoyproxy.io/) が推奨されています。従って、grpc-webの記事を検索すると、envoyのconfig yamlを作成してgrpc-webを受けられるようにしている例がたくさん見つかります（先ほど上記で紹介したわかり易い記事でも[その形で](https://qiita.com/Le0tk0k/items/af2fbf1d246f0a788ec3#%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7%E3%81%AE%E8%A8%AD%E5%AE%9A)実装されていますね）。

今回はkubernetes環境上で、istioを使っている場合に、istio-proxy（実態はenvoy）の設定に変更を加えて、grpc-webを受けられるようにする方法をご紹介します。

## 1. gRPCサーバの準備

バックエンドのgRPCサーバは公式のgreeting severサンプルを使用します。

https://grpc.io/docs/languages/go/quickstart/#get-the-example-code

kubernetes上に立てて、リクエストを受けられるようにするために、まずDockerfileを書きます。

```dockerfile:dockerfile
FROM golang:1.15 AS build-env

RUN cd $GOPATH/src && git clone -b v1.34.0 https://github.com/grpc/grpc-go
WORKDIR $GOPATH/src/grpc-go/examples/helloworld

RUN go get -d ./greeter_server/...
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/greeter_server ./greeter_server/main.go

FROM alpine:latest
COPY --from=build-env /bin/greeter_server /bin/
CMD ["/bin/greeter_server"]
```

試しにビルドして立ててみて、[evans](https://github.com/ktr0731/evans)でお手軽動作確認。

```bash
❯ docker build -t greeter-server:latest .
❯ docker run -it --rm -p 50051:50051 greeter-server:latest
```

```bash
❯ evans repl --host localhost --port 50051 --proto ./helloworld.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


helloworld.Greeter@localhost:50051> call SayHello
name (TYPE_STRING) => hoge
{
  "message": "Hello hoge"
}
```

server log

```bash
2020/12/21 15:58:49 Received: hoge
```

いけてるいけてる。

## 2. local環境でgrpc-webしてみる

docker-compose等で、envoyと共に立てる場合には[このように](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web/-/blob/master/docker-compose.yaml#L8-18)`docker-compose.yaml`と[envoyのconfig yaml](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web/-/blob/master/envoy.yaml)を用意します。

またテストクライアントですが、今回はgrpc-webのテスト用に[こちらのpackage](https://github.com/improbable-eng/grpc-web)を使って下記のコマンドでjs/tsのclient codeを生成しました。

```bash
npm install ts-protoc-gen

protoc -I. \
    --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
    --js_out="import_style=commonjs,binary:./grpc-web" \
    --ts_out="service=grpc-web:./grpc-web" \
    ./helloworld.proto
```

そして、ざっくりと`client.ts`を書きます

```ts:client.ts
import { HelloRequest, HelloReply } from './grpc-web/helloworld_pb'
import { GreeterClient, ServiceError } from './grpc-web/helloworld_pb_service'
import { grpc } from '@improbable-eng/grpc-web'
import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport';

const client = new GreeterClient('http://localhost:50051', {
    transport: NodeHttpTransport()
})

const requestMessage = new HelloRequest()
requestMessage.setName("grpc-web!!")

client.sayHello(requestMessage, new grpc.Metadata(), (error: ServiceError | null, reply: HelloReply | null) => {
    if (error != null) {
        console.log('code: %d, message: %s.', error.code, error.message)
    } else if (reply !== null && error == null) {
        console.log('message: %s.', reply.getMessage())
    }
})
```

実際にはブラウザからのアクセスですが、今回は都合により[Node.js](https://nodejs.org/en/)でアクセスします。

まずバックエンドサーバとプロキシを立てて、

```bash
docker-compose up app envoy
```

[tsc](https://www.typescriptlang.org/docs/handbook/compiler-options.html)でコンパイルした先ほどのクライアントをlocal環境から実行すると、

```bash:client
❯ npm run start

> grpc-web-sample@ start /Users/hoge/grpc-web-sample
> node client.js

message: Hello grpc-web!!.
```

```bash:server
greeting-server | 2020/12/21 16:13:27 Received: grpc-web!!
```

返ってきましたー :clap: 

なお、envoyを立てなかったり、grpc-web filter設定を入れないと、こんなエラーに。

```bash:no-envoy
code: 2, message: Response closed without headers.
```

```bash:without-grpc-web-filter
code: 14, message: .
```
https://grpc.github.io/grpc/core/md_doc_statuscodes.html
によると、ステータスコード２は`UNKNOW`（よく見る）、14は`UNAVAILABLE`ですね。

## 3. istio環境下でテスト

では、いよいよkubernetesのistio環境でテストしてみます。
istioはdefaultプロファイルで`istioctl install`しただけの状態です。

```bash
$ istioctl version --remote=false
1.7.4
```

```bash
$ k get po -n istio-system
NAME                                    READY   STATUS    RESTARTS   AGE
istio-ingressgateway-66f459f859-vdcvt   1/1     Running   0          56s
istiod-6869899d55-fxp7f                 1/1     Running   0          72s
```

まず、ネームスペースを作って、そこにistio-proxyをinjectする設定をします。

```bash
$ k create ns api-test
namespace/api-test created
$ kubectl label namespace api-test istio-injection=enabled
namespace/api-test labeled
$ k get ns api-test -o yaml
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2020-12-21T00:32:44Z"
  labels:
    istio-injection: enabled
  name: api-test
  resourceVersion: "267020212"
  selfLink: /api/v1/namespaces/api-test
  uid: cbd1da08-9df0-4f53-b8e2-25b07b6f7ade
spec:
  finalizers:
  - kubernetes
status:
  phase: Active
```

次に、[こんな感じで作ったdeploymentとservice](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web/-/blob/master/manifests/greeting-server.yaml)をデプロイします。

```bash
$ k apply -f greeting-server.yaml -n api-test
deployment.apps/greeting-server created
service/greeting-server-headless created
$ k get po -n api-test
NAME                               READY   STATUS    RESTARTS   AGE
greeting-server-79d997d79f-pshbx   2/2     Running   0          13s
$ k get svc -n api-test
NAME                       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
greeting-server-headless   ClusterIP   None         <none>        50051/TCP   8m22s
```

describeしてみて、istio-proxyがsidecarされていることを確認。

```yaml
Name:         greeting-server-79d997d79f-pshbx
Namespace:    api-test
<中略>
Init Containers:
  istio-init:
    Container ID:  docker://6414893e192f896f3dbe4c31fbea7d135e9ad1367bf6c823820eaf587e026d7a
    Image:         docker.io/istio/proxyv2:1.7.4
    Image ID:      docker-pullable://istio/proxyv2@sha256:17faf9ddc1254ad98cc70fb11fa74043ce2705f3272eace3fa7011a29576c8f1
<中略>
Containers:
  greeting-server:
    Container ID:   docker://c9ac9d055f032370155fde1335e84d70f6650920be2e9e453bf0812e980a823b
    Image:          registry.gitlab.com/yo-c-ta/istio-proxy-grpc-web:master
    Image ID:       docker-pullable://registry.gitlab.com/yo-c-ta/istio-proxy-grpc-web@sha256:24dfe86b1c56b0fe1e8ec2ddc2e719a02d92ee0abfe8359be65374fc12970710
    Port:           50051/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 21 Dec 2020 00:36:49 +0000
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-rmw4x (ro)
  istio-proxy:
    Container ID:  docker://34c6ff5bb312bd9cb3eae94ea13d176819be99058e8cc7b43175346fd7b13b3a
    Image:         docker.io/istio/proxyv2:1.7.4
    Image ID:      docker-pullable://istio/proxyv2@sha256:17faf9ddc1254ad98cc70fb11fa74043ce2705f3272eace3fa7011a29576c8f1
    Port:          15090/TCP
    Host Port:     0/TCP
    <中略>
    State:          Running
      Started:      Mon, 21 Dec 2020 00:36:49 +0000
    Ready:          True
    Restart Count:  0
<後略>
```

続いて、[istioのgatewayとvirtualserviceリソースを作成](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web/-/blob/master/manifests/isitio-gateway.yaml)して、istio-ingessgatewayから入ってきたリクエストがgreeting-serverにルーティングされるようにします。

```bash
$ k apply -f istio-gateway.yaml -n api-test
gateway.networking.istio.io/grpc-api-gateway created
virtualservice.networking.istio.io/greeting-server created
$ k get gateway -n api-test
NAME               AGE
grpc-api-gateway   8s
$ k get virtualservice -n api-test
NAME              GATEWAYS             HOSTS   AGE
greeting-server   [grpc-api-gateway]   [*]     19s
```

istio-ingressgatewayのExternal IPを確認。

```bash
$ k get service -n istio-system
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)                                                           AGE
istio-ingressgateway   LoadBalancer   172.21.96.79    35.299.212.186   15021:30009/TCP,30070:30259/TCP,30130:30260/TCP,50051:31845/TCP   41d
istiod                 ClusterIP      172.21.81.209   <none>           15010/TCP,15012/TCP,443/TCP,15014/TCP,853/TCP                     41d
```

試しに、gRPCでアクセスしてみましょう。
evansにはcli modeもあるので、今度はそちらで（気分です）。

```bash
$ echo '{ "name": "via istio-ingressgateway" }' | ./evans --host 35.299.212.186 --proto ./helloworld.proto cli call helloworld.Greeter.SayHello
{
  "message": "Hello via istio-ingressgateway"
}
```

ちゃんとリクエストできているようです。

では、EnvoyFilterを作ってデプロイしましょう。

```yaml:filter.yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: grpc-web-filter
spec:
  workloadSelector:
    labels:
      app: greeting-server
      release: greeting-server
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.grpc_web # <- ここが味噌

```

```bash
$ k apply -f filter.yaml -n api-test
envoyfilter.networking.istio.io/grpc-web-filter created
$ k get EnvoyFilter -n api-test
NAME              AGE
grpc-web-filter   13s
```

できましたね。
この状態で、先ほどlocal環境で使用したgrpc-webクライアントからリクエストを送ってみます。
宛先hostは`localhost`からistio-ingressgatewayのExternal IPに変更してください。

```bash
$ npm run start

> start
> node client.js

message: Hello grpc-web!!.
```

ちゃんと返ってきましたね！ :tada:
サーバ側にもログが出ていることが確認できます。

```bash
$ k logs greeting-server-79d997d79f-kwb28 -n api-test -c greeting-server
2020/12/22 02:44:52 Received: via istio-ingressgateway # <- 少し前のgrpcのリクエスト
2020/12/22 02:51:48 Received: grpc-web!!               # <- grpc-webのリクエスト
```

## まとめ

いかがでしたでしょうか。
istio-proxyでgrpc-webする際には、istioのEnvoyFilterカスタムリソースを作る必要があるのですが、実際に実装している例があまり見つからず、今回の記事を書くことにしました([この記事](https://venilnoronha.io/seamless-cloud-native-apps-with-grpc-web-and-istio)が見つかったのですが、istioのversionが結構古いです)。

なお、isito公式のEnvoyFilterに関するページは[こちら](https://istio.io/latest/docs/reference/config/networking/envoy-filter/)。
今回使用したコードは[こちら](https://gitlab.com/yo-C-ta/istio-proxy-grpc-web)にまとめています。

誰かの役に立てば幸いです :innocent: