7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

istio-proxy で grpc-web する

Last updated at Posted at 2020-12-22

TL;DR :pencil:

istio-proxy で grpc-web する場合は EnvoyFilter カスタムリソースを作って、 envoy.grpc_web を追加する。
こんな感じで。

はじめに

gRPCはHTTP2上でprotocol buffersを使用し、Remote procedure call する枠組みで、grpc-webはブラウザ上からgRPCサーバに対してアクセスするための方法です。gRPCでWebAPIを開発するにあったって、grpc-webは(執筆時点では)無くてはならない存在です。

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

さて、そんなgrpc-webでgRPCサーバにアクセスするためには、(再び執筆時点で) special proxy が必要で、 Envoy が推奨されています。従って、grpc-webの記事を検索すると、envoyのconfig yamlを作成してgrpc-webを受けられるようにしている例がたくさん見つかります(先ほど上記で紹介したわかり易い記事でもその形で実装されていますね)。

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

1. gRPCサーバの準備

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

kubernetes上に立てて、リクエストを受けられるようにするために、まず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でお手軽動作確認。

❯ docker build -t greeter-server:latest .
❯ docker run -it --rm -p 50051:50051 greeter-server:latest
❯ 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

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

いけてるいけてる。

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

docker-compose等で、envoyと共に立てる場合にはこのようにdocker-compose.yamlenvoyのconfig yamlを用意します。

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

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を書きます

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でアクセスします。

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

docker-compose up app envoy

tscでコンパイルした先ほどのクライアントをlocal環境から実行すると、

client
❯ npm run start

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

message: Hello grpc-web!!.
server
greeting-server | 2020/12/21 16:13:27 Received: grpc-web!!

返ってきましたー :clap:

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

no-envoy
code: 2, message: Response closed without headers.
without-grpc-web-filter
code: 14, message: .

https://grpc.github.io/grpc/core/md_doc_statuscodes.html
によると、ステータスコード2はUNKNOW(よく見る)、14はUNAVAILABLEですね。

3. istio環境下でテスト

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

$ istioctl version --remote=false
1.7.4
$ 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する設定をします。

$ 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をデプロイします。

$ 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されていることを確認。

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リソースを作成して、istio-ingessgatewayから入ってきたリクエストがgreeting-serverにルーティングされるようにします。

$ 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を確認。

$ 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もあるので、今度はそちらで(気分です)。

$ 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を作ってデプロイしましょう。

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 # <- ここが味噌

$ 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に変更してください。

$ npm run start

> start
> node client.js

message: Hello grpc-web!!.

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

$ 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カスタムリソースを作る必要があるのですが、実際に実装している例があまり見つからず、今回の記事を書くことにしました(この記事が見つかったのですが、istioのversionが結構古いです)。

なお、isito公式のEnvoyFilterに関するページはこちら
今回使用したコードはこちらにまとめています。

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

7
4
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
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?