TL;DR
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を書きます。
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.yaml
とenvoyの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
を書きます
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環境から実行すると、
❯ npm run start
> grpc-web-sample@ start /Users/hoge/grpc-web-sample
> node client.js
message: Hello grpc-web!!.
greeting-server | 2020/12/21 16:13:27 Received: grpc-web!!
返ってきましたー
なお、envoyを立てなかったり、grpc-web filter設定を入れないと、こんなエラーに。
code: 2, message: Response closed without headers.
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を作ってデプロイしましょう。
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!!.
ちゃんと返ってきましたね!
サーバ側にもログが出ていることが確認できます。
$ 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に関するページはこちら。
今回使用したコードはこちらにまとめています。
誰かの役に立てば幸いです