Istio ~EnvoyFilter入門~
About
# 検証当時の環境:OpenShift ServiceMesh
imageRegistry: registry.redhat.io/openshift-service-mesh
imageTag: 2.1.4
"ISTIO_PROXY_VERSION": "1.9.9"
記事の目的
Istioを使う中でいまいちわかりづらく、利用を避けてしまう(と思っている)EnvoyFilter。
様々に拡張性が得られそうだなと思いつつも、Envoyの実装と仲良くないとなかなか踏み込めない領域かつ、この機能を使うための全体像や解説がいまいち少ない(すでに使いこなしている人がexampleやgitのissueで挙げている程度)かなという感想を抱いたので、自分なりに実装方法を調べて覚書程度にメモしておこうと思います。
Istioとは
わざわざまとめるほど理解しているわけではないので、公式から引用。
https://istio.io/latest/about/service-mesh/
OSSのサービスメッシュソフトであり、セキュリティ(認証認可や暗号化通信)、トラフィックコントロール、モニタリングといった特徴を持つレイヤーを、既存の分散システムの上に構築することができる。
大まかに意訳するとこんな感じかなと思います。
Envoyとは
こちらも公式を引用。
https://www.envoyproxy.io/
OSSの、クラウドネイティブなアプリケーションのために開発されたプロキシー。
IstioにおいてはIngress/Egress Gateway (Edge Proxy)と、アプリケーションのsidecar (Service Proxy)として利用される。
今回注目したいのはIstioの中でも中核の機能と呼べるこのプロキシーをカスタマイズする機能の一つ、"EnvoyFilter"について。
ただしEnvoyFilter含め、IstioがCRDで扱うAPIリソースの設定内容を各sidecarに反映する仕組みはxDS(xxx Discovery Service)というものを用いており、特にEnvoyFilterを用いる場合はこのxDSについての理解が不可欠と思いますので、まずはその点から触れていきます。
xDS
Envoyがリアルタイムにmanagement serverから設定内容を取得し自身に反映させる仕組み。
Envoyがリアルタイムに設定を反映させる(dynamic configuration)仕組みは、以下の3つが存在します。
- file system pathの監視(inotify/kqueue)
- gRPC stream通信
- REST-JSON APIのpolling
後者二つはDiscoveryRequestという形式のprotocol bufferもしくはJSONに沿ったpayloadを利用してやり取りをし、全ての方法においてDiscoveryResponseという形式のレスポンスを受け取ります。
Istioにおいては最初の2つ目のgRPC通信でDynamicConfigurationを行っていますが、その際sidecarのメインプロセスであるpilot-agentがプロキシとして機能し、sidecarのファイルシステムに作成するUNIXソケット(/etc/istio/proxy/XDS|SDS、EnvoyからxDSを行う際に指定するエンドポイント)への通信を、management server(istiod)へそのまま転送してstreaming通信を行っております。
gRPC通信について、byteレベルで仕様を理解しようという記事も書いているので興味のあるかたは覗いてみてください。
更に情報のやり取りをするプロトコルは以下の4つが存在します。
- State of the World(SotW) & resource typeごとのgRPC stream
- incremental xDS & resource typeごとのgRPC stream
- SotW & ADS(*) stream
- incremental xDS & ADS stream
(*) ADS: Aggregated Discovery Service, Eventual consistency considerations より、独立したCDSやRDSのupdateにより起きうるtraffic dropを防ぐため、関連しあうxDSをまとめ、段階的にenvoyの設定を更新するための仕組み。gRPC streamが独立して設立された際に、それぞれのレスポンスの整合性をとるという手間を省く狙いもあるらしい。
State of the Worldとは、あるxDSのリソースを更新したら、クライアントがsubscribeしているそのxDS全量をmanagement serverから送信するもの。
IstioではBootstrapConfigより、configs[].bootstrap.node.dynamic_resources.ads_config.api_type="GRPC"
-> Deltaではなく、かつADS Configを使っていることから、SotW gRPC & ADSを用いているということが分かります。
またここで参照しているClusterが"xds-grpc" -> endpointsが ./etc/istio/proxy/XDS
へのパイプになっており、pilot-agentの実装からgRPC serverのことが特定できます。
Envoyの現行設定のダンプAPI( GET /config_dump
)を用いると、Istioの管理する最上位のResource Typeは以下であることが確認できます。
$ oc exec -ti -n istio-system $(oc get po -n istio-system -l app=istio-ingressgateway -o jsonpath='{.items[0].metadata.name}') -- curl localhost:15000/config_dump | jq -r '.configs[] | .["@type"]'
type.googleapis.com/envoy.admin.v3.BootstrapConfigDump
type.googleapis.com/envoy.admin.v3.ClustersConfigDump
type.googleapis.com/envoy.admin.v3.ListenersConfigDump
type.googleapis.com/envoy.admin.v3.ScopedRoutesConfigDump
type.googleapis.com/envoy.admin.v3.RoutesConfigDump
type.googleapis.com/envoy.admin.v3.SecretsConfigDump
$
その他の形式については以下の公式ドキュメント(ry
https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol
EnvoyFilter
ここまでで、Envoyの設定の概要について触れてきました。
ここからようやくEnvoyFilterの話に移っていきたいと思います。
Terminology
- downstream
- Envoyから見た接続元
- upstream
- Envoyから見た接続先
- IngressGateway(Edge)などではMesh内のService等、Sidecarではlocalhostが主な接続先となる
-
Filter
- Envoyの中でリクエストを処理する際に適用される各種関数機能
- 設定した順にシーケンシャルに適用されるパイプライン方式
- リクエストを処理する各段階で適用されるが、今回扱うHTTP通信の中で関連性があるものは、適用される順にListener Filter、Network Filter、HTTP Filter、Upstream Filterとなる。
- Network Filterの最後は、転送先またはレスポンスが決定されるtcp_proxyまたはhttp_connection_manager(HTTP RouteがRDS等で必ず定義される)となる。
- 中核となるNetwork FilterおよびHTTP FilterはVirtual Serviceと関連性が高い
- Listener
- Downstreamから接続される際のVirtualHost等の定義をする
- TLS Terminationや適用されるNetworkFilterの定義が含まれる
- Gatewayと関連性が高い
- Cluster
- Upstreamを定義する
- エンドポイントの情報、TLSクライアント情報、ロードバランシング、Circuit Breaker、ヘルスチェック等の設定が可能
- DestinationRule、ServiceEntry、WorkloadEntryと関連性が高い
リクエスト処理フロー
かなり大雑把な処理の流れとしては、
- リクエストをListenerで受け入れる
- 各Filter適用
- 最終的に特定のCluster(もしくはDirectResponseのような特定のFilter)に到達し、upstreamにリクエストを転送
- upstreamからのレスポンスを受け取り、今度はレスポンスに対して有効なFilterを逆順に適用させ、downstreamに返却する
のようになります。
EnvoyFilter on Istio
EnvoyFilterとは、その名の通りFilter機能を任意のEnvoyに付け替えできる機能であり、Istioの各種設定用のAPI定義よりは抽象度が落ちて生のXDSを定義するようなイメージになります。
GatewayとSidecarといった各Envoyの役割、およびFilterがどこにいるのか、KubernetesのService等でどのように設定されてしまうのかといった点が抑えられれば、設定すべき内容が見えてくると思います。
実際の設定例を見てみるとよりイメージしやすくなるかと思いますので、比較的最近VirtualServiceで取り込まれたDirectResponseのEnvoyFilterにおける実装をいくつかのパターンで例示してみたいと思います。
前提
以降紹介する実装例においては、以下のようなリソースが作成されているものと仮定します。
1. 8080ポートでPlain HTTPをListenしているNginxコンテナ
$ oc get po -l deployment=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-64d4474bf5-vdn2c 2/2 Running 0 6d 10.131.0.30 ip-10-0-200-55.ec2.internal <none> <none>
$ oc get svc,ep -l app=nginx -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/nginx ClusterIP 172.30.52.57 <none> 8080/TCP,8443/TCP 6d deployment=nginx
NAME ENDPOINTS AGE
endpoints/nginx 10.131.0.30:8080,10.131.0.30:8443 6d
$
# debug podはlabel等を持たせず、Serviceからの割り振り対象にしない。かつannotationを保持することでsidecar injectionを有効にする。
$
$ oc debug po/nginx-64d4474bf5-vdn2c -c nginx --keep-labels=false --keep-annotations=true
Starting pod/nginx-64d4474bf5-vdn2c-debug ...
Pod IP: 10.131.0.40
If you don't see a command prompt, try pressing enter.
sh-4.4$
sh-4.4$ curl -s nginx:8080 -D-
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sun, 19 Mar 2023 14:16:52 GMT
Content-Type: text/html
Content-Length: 60
Last-Modified: Sat, 28 May 2022 22:07:12 GMT
Connection: keep-alive
ETag: "62929d10-3c"
Accept-Ranges: bytes
<html>
<body>
<div>Hello from Nginx</div>
</body>
</html>
sh-4.4$
2. Istio関連(Mesh内)リソース
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: directresponse-internal
labels:
self.test: "true"
spec:
gateways:
- mesh
hosts:
- nginx.test.direct
http:
- match:
- port: 8080
uri:
prefix: /-/
route:
- destination:
host: nginx
port:
number: 8080
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: stub-entry-nginx
labels:
self.test: "true"
spec:
exportTo:
- .
- istio-system
hosts:
- nginx.test.direct
ports:
- name: http
number: 8080
protocol: HTTP
resolution: DNS
3.Istio関連(Gateway)リソース
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: directresponse-external
labels:
self.test: "true"
spec:
gateways:
- directresponse
hosts:
- nginx.test.direct.external
http:
- match:
- port: 443
uri:
prefix: /-/
route:
- destination:
host: nginx
port:
number: 8080
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: ingressgateway-entry-nginx
labels:
self.test: "true"
spec:
exportTo:
- .
- istio-system
hosts:
- nginx.test.direct.external
ports:
- name: https
number: 443
protocol: HTTPS
resolution: DNS
endpoints:
- address: istio-ingressgateway.istio-system.svc.cluster.local
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: directresponse
labels:
self.test: "true"
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- nginx.test.direct.external
port:
name: https
number: 443
protocol: HTTPS
tls:
credentialName: nginx # 証明書の作成方法はさまざまにガイドが出ているので割愛
mode: SIMPLE
※リソース全体
$ oc get $(oc api-resources --api-group=networking.istio.io -o name | sed ':t;N;$!bt;s/\n/,/g') -l self.test=true
NAME AGE
gateway.networking.istio.io/directresponse 23m
NAME HOSTS LOCATION RESOLUTION AGE
serviceentry.networking.istio.io/ingressgateway-entry-nginx ["nginx.test.direct.external"] DNS 23m
serviceentry.networking.istio.io/stub-entry-nginx ["nginx.test.direct"] DNS 23m
NAME GATEWAYS HOSTS AGE
virtualservice.networking.istio.io/directresponse-external ["directresponse"] ["nginx.test.direct.external"] 23m
virtualservice.networking.istio.io/directresponse-internal ["mesh"] ["nginx.test.direct"] 23m
$
ServiceEntryを resolution=DNS
で設定することにより、架空のホスト名に対してServiceMesh内でのみ有効なIP(240.240.0.0/16)を割り振り、それに対してRoute(VirtualService定義)等を設定することが可能となります。
これらの設定があることで、例えば以下のようなリクエストを飛ばすことが可能となります。
Sidecar同士の通信
# sh-4.4$ uname -n
# nginx-64d4474bf5-vdn2c-debug
# ServiceEntryで指定した架空のFQDNに対してリクエスト
# Route設定ありの宛先(prefix="/-/")の場合:upstream(nginx)からレスポンスが返る(特にドキュメント置いたりしていないので404)
sh-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 404 Not Found
server: envoy
date: Sun, 19 Mar 2023 14:46:48 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 1
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
sh-4.4$
# Route設定なしの宛先の場合:envoyからRouteMatchに当てはまらないため、404レスポンスが返る
sh-4.4$ curl nginx.test.direct:8080/ -D-
HTTP/1.1 404 Not Found
date: Sun, 19 Mar 2023 14:47:47 GMT
server: envoy
content-length: 0
sh-4.4$
IngressGatewayを通した通信
# ingress gatewayにおいて、443において適当な証明書でHTTPS通信をGatewayで設定している。
# そのため、https://nginx.test.direct.externalというURLで通信が可能。
# さらに補足で、HTTPS通信の場合、curl側で何もしなくでもTLSv1.3の拡張仕様であるALPNを用いて通信が行われ、Envoy側でもHTTP/2通信をサポートしているため、自動的にUpgradeが行われる。
# Route設定ありの宛先の場合:Sidecar同士の通信同様upstream(nginx)からレスポンスが返る(特にドキュメント置いたりしていないので404)
bash-4.4$ curl https://nginx.test.direct.external/-/direct -k -D-
HTTP/2 404
server: istio-envoy
date: Mon, 20 Mar 2023 02:36:06 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 2
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
# Route設定なしの宛先の場合:こちらもSidecar同士の通信同様envoyからRouteMatchに当てはまらない場合の404レスポンスが返る
bash-4.4$ curl https://nginx.test.direct.external/ -k -D-
HTTP/2 404
date: Mon, 20 Mar 2023 02:36:00 GMT
server: istio-envoy
bash-4.4$
ここまでで、構築した環境は以下のようになります。
IngressGatewayもMeshMemberですが、区別のためroot Namespace以外のMeshMemberということを図の中で表現しております。
本題
EnvoyFilter実装例
ここからやっとEnvoyFilterの実装例に移っていきます。
上にも書きましたがDirectResponseという、Envoyで直接レスポンスを生成するFilterの実装例をいくつか紹介します。
ex)1. Routeでの実装例(Outbound)
NetworkFilterの中でも一般的に用いられるhttp_connection_managerのRoute定義での実装例となります。
Virtual Serviceで実装した場合と同じような状態になります。
Sidecar同士の通信に適用してみます。
設定イメージ
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: direct-response-403-route1
labels:
self.test: "true"
spec:
configPatches:
- applyTo: HTTP_ROUTE #<-*1
match: #<-*2
context: SIDECAR_OUTBOUND #<-*3
routeConfiguration:
name: "8080"
vhost:
name: nginx.test.direct:8080
patch:
operation: INSERT_FIRST #<-*4
value:
name: direct-reply-path
match:
prefix: /-/direct
direct_response:
status: 403
body:
inline_string: |
{
"msg":"this response directory generated by config.route.v3.Route (ptn1)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
.spec.configPatchesの各値について以下に補足
- *1) applyTo=HTTP_ROUTE -> HTTP_Connection_ManagerのRoute定義に対するFilterを定義。
- *2) match -> HTTP_ROUTEはHTTP_Connection_Managerのconfig.route.v3.RouteConfiguration FilterにおけるVirtualHostに対して設定される。そのため"8080"という名前(Istioが自動的に命名)のRouteConfigurationの中で定義されている"host名:port"のVirtualHost(こちらもIstioが自動的に命名)に適用させる。
- *3) match.context=SIDECAR_OUTBOUND -> MeshMemberの中のsidecar発信の通信(IstioではOutbound方向に多くのFilterが適用されるように構成される。inbound方向ではtcp_proxyというNetwork Filterが一律適用されており、工夫しないと適用できない(*1))。また、GATEWAYと定義し転送先のアプリのポート名を持つRouteConfigurationと、転送先のホスト名を持つVirtualHostに対して設定すれば外部からの通信に対して適用できるようになる。(今回はEnvoyFilterの設定方法に焦点を当てたいので、同じような設定内容になるものは割愛)
- *4) patch.operation=INSERT_FIRST -> Filterは上から順に適用されていくため、元々の定義(全てのパスがCluster(nginx.test.direct)に到達する)の前に適用されるように最初に差し込む
(*1)こちらの設定方法は本投稿の二つ目の例を参照
設定イメージで示したように特定のRoute定義に達した際、条件に合致したHTTPリクエストに対し、upstreamへの転送前にレスポンスがEnvoyで生成され、downstreamに返却されることになります。
仮に Envoy(nginx) に対するリクエストに対して適用した場合、
- 外部からの通信においては Envoy(IngressGateway) の中で処理が完結
- MeshMember内部での通信の場合、送信元の Envoy(nginx_debug) の中で処理が完結
のようになります。
実際に動作を見てみます。
#bash-4.4$ uname -n
#nginx-64d4474bf5-vdn2c-debug
# Envoy(nginx_debug)からの通信
# DirectResponseを適用したパスに対してリクエストをすると、自分自身のサイドカーから設定した403のレスポンスが返ってくる
# 本当に自分自身から帰ってきているのかといったことはサイドカーのアクセスログ等を確認
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Mon, 20 Mar 2023 05:44:33 GMT
server: envoy
{
"msg":"this response directory generated by config.route.v3.Route (ptn1)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
# それ以外のパスに対しては接続先のnginxから404が返ってくる
bash-4.4$ curl nginx.test.direct:8080/-/indirect -D-
HTTP/1.1 404 Not Found
server: envoy
date: Mon, 20 Mar 2023 05:46:34 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 4
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
図で示したように、今回Gatewayに対しては設定していないのと、そもそもホスト名も違うのでIngressGatewayを経由した場合はDirectResponseは有効になりません。
# ingress gatewayのログを確認することで、経由していることを確認できる
bash-4.4$ curl https://nginx.test.direct.external/-/direct -k -D-
HTTP/2 404
server: istio-envoy
date: Tue, 21 Mar 2023 01:24:44 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 2
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
また、workloadSelector等で指定していないので、今回ターゲットにしているEnvoy(nginx) 自身からリクエストを飛ばしてもDirectResponseの挙動が確認できます。
#bash-4.4$ uname -n
#nginx-64d4474bf5-vdn2c
# Envoy(nginx)からの通信
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Tue, 21 Mar 2023 01:29:07 GMT
server: envoy
{
"msg":"this response directory generated by config.route.v3.Route (ptn1)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
bash-4.4$
但しOutbound方向にしか効かない設定になるので、クラスター内でsidecarを持たないクライアントがリクエストを飛ばしてきた場合等は効かないという結果になります。
これを回避するためには Envoy(nginx) の中で、localhostへのInbound通信(sidecarにとっては自分自身がくっついているコンテナがInbound通信のupstreamとなる)に対し、同じようなRoute定義を作成してあげる必要があります。
ex)2. Routeでの実装例(Inbound)
1つ目の例ではEnvoyを持つOutbound方向の通信がProtocol SelectionにおいてHTTP通信であった場合、HTTP_ROUTEが定義されているのでそのままルールを追加すればよく、比較的容易な例となりました。
今回はInboundに適用させるのですが、Istioの発行するXDSではInbound通信は一律transport layerに適用されるTCP ProxyというNetworkFilterになってしまっております。
またこのTCP Proxyや適用させたいHTTP connection managerはexclusiveなFilterとなるため、例えば「HTTP Connection manager Filterに当てはまらなかった場合はTCP Proxyにする」といった設定は出来ません。
HTTP通信用のDirectResponseを適用させるにはTCP Proxy Filterを削除し、手動でHTTP Connection Managerを定義する必要があります。
今回Inboundに対して設定する際にあえて実装として今一つな例と、実用に耐えうる例の2つを載せてみたいと思います。
今一つな例の方は、設定する箇所がenvoyにとっての通信方向の考え方を理解するのにいい例かなと感じたので最初に載せます。
設定イメージ
1例目:今一つな実装例
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: direct-response-403-route2
labels:
self.test: "true"
spec:
workloadSelector: #<-*1
labels:
deployment: nginx
configPatches:
- applyTo: ROUTE_CONFIGURATION
match:
context: SIDECAR_OUTBOUND #<-*2
patch:
operation: MERGE #<-*3
value:
name: "8080" #<-*4
virtual_hosts:
- name: "nginx.direct_response:8080"
include_request_attempt_count: true
domains: #<-*5
- "nginx"
- "nginx:8080"
- "nginx.test.direct"
- "nginx.test.direct:8080"
- "nginx.test.direct.external"
- "nginx.test.direct.external:443"
routes:
- name: direct_response
match:
prefix: "/-/direct"
direct_response:
status: 403
body:
inline_string: |
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
- name: allow_any
match:
prefix: "/"
route:
cluster: "inbound|8080||" #<-*6
timeout: 0s
max_stream_duration:
max_stream_duration: 0s
- applyTo: NETWORK_FILTER
match: #<-*7
listener:
portNumber: 8080
filterChain:
filter:
name: envoy.filters.network.tcp_proxy
context: SIDECAR_INBOUND #<-*8
patch:
operation: REPLACE
value:
name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: direct_responser_route
rds: #<-*9
route_config_name: "8080"
config_source:
ads: {}
resource_api_version: V3
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
server_name: direct-responser
access_log: #<-*10
- name: envoy.access_loggers.file
typed_config:
"@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"
path: /dev/stdout
log_format:
text_format_source:
inline_string: "[%START_TIME%] \"%REQ(:authority)%\" direct_response_ptn2: passed replaced NWfilter\n"
.specの各値について以下に補足
- *1) workloadSelector -> IstioのADSは何もしなければServiceMesh全体にブロードキャストされてしまうが、適用範囲を絞る方法がいくつか存在する。今回は特定のインスタンスのInboundに対するルーティングルールのみ変更したいのでworkloadSelectorで指定。
- *2) configPatches.match.context=SIDECAR_OUTBOUND -> ROUTE_CONFIGURATIONやCLUSTERは、常にOUTBOUND方向となる(たとえその通信が自分目掛けて入ってきた通信であっても、sidecar envoyから見てlocalhostも常にupstreamとなるため)
- *3) configPatches.patch.operation=MERGE -> Istioの制約により、ROUTE_CONFIGURATIONはMERGEしかサポートされていない。そのため新しいROUTE_CONFIGRATION定義をInbound通信用に加えてRDSで指定するといったことができないという制約が発生する。
- *4) name="8080" -> Istioでは、ROUTE_CONFIGURATIONはMesh内で定義されているServiceやServiceEntryの定義に従いそのポート番号の名前で作成され、さらに内部でTransport Layerのプロトコルやvirtual_hostsに指定するドメイン(リクエストのHostヘッダー (HTTP/1.1)、SNI (TLS)、:authority(HTTP/2))によってルーティング定義を割り当てる。今回8080ポートの自分に目掛けてくる通信が対象のため、"8080"というnameを指定
- *5) virtual_hosts.domains -> catch_all(ワイルドカードドメイン)だと、自分以外の8080番ポートへの通信も今回作成するRoute定義の対象となってしまう。そのため自分自身に向けてくるであろうドメイン名を明示的に指定する。指定出来ていないドメインで自分に通信が来るとループが起こって通信不能となり、今一つとなっているポイントである
- *6) route.cluster="inbound|8080||" -> もともと自分自身への通信は、
"inbound|<port>||"
という名前で定義されるCluster定義でlo I/F(127.0.0.1 or ::1)に転送される。定義は当てはまった時点で適用されるため、direct_responseさせたいMatch定義を上に置き、それ以外の通信は自分自身に通すように設定する。 - *7) match.listener -> 多くの場合Serviceで定義した内容(.spec.ports)に対するリクエストを"inbound|||"のClusterにTCP_Proxy Filterで転送する定義が追加される。その定義にmatchさせ、REPLACEを実施する。
- *8) configPatches.match.context=SIDECAR_INBOUND -> Listener等はInboundのContextとなる。その定義を転送する定義を行うRouteやClusterはOutboundのContextとなる。
- *9) typed_config.rds -> 別途定義したROUTE_CONFIGURATIONの定義を用いるため、RDSを用いる。
- *10) access_log -> 今回のようにhttp_connection_managerや、その他各ProxyのNETWORK_FILTERの中にもaccess_logは定義可能で、当然ながらそれぞれ使えるフィールドが異なる。
補足の中に記載したように、既存の定義体を使いまわそうとすると全ての自分宛の通信に使われるドメインを明示的に指定している必要があります。しないと、
-
PassthroughCluster
というClusterに合致 - 宛先が
type=ORIGINAL_DST
となっているため、元の接続先(例えば、Pod IP)に転送される - Istioの作成したiptables定義によって再度sidecar envoy(自分自身)に到着
- 1.に戻る
というようにリクエストがループしてしまってサーバー側に到着せず503エラーとなります。
この問題含め、上記の設定をした際の挙動を見てみます。
#bash-4.4$ uname -n
#nginx-64d4474bf5-vdn2c-debug
# Envoy(nginx_debug)からリクエスト
# 今までの宛先:DirectResponseが有効
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 03:34:48 GMT
server: envoy
x-envoy-upstream-service-time: 5
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
# DirectResponse以外のパスへのリクエストも問題なくサーバー側まで届く
bash-4.4$ curl nginx.test.direct:8080/-/index -D-
HTTP/1.1 404 Not Found
server: envoy
date: Fri, 24 Mar 2023 04:02:32 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 1
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
# Envoy(IngressGateway)を通した場合
# 設定イメージで示したようにex.1)の時と異なりDirectResponseが有効となる
bash-4.4$ curl https://nginx.test.direct.external/-/direct -k -D-
HTTP/2 403
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 04:04:10 GMT
server: istio-envoy
x-envoy-upstream-service-time: 6
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
bash-4.4$
# こちらも問題なくDirectResponse以外のパスも有効
bash-4.4$ curl https://nginx.test.direct.external/-/index -k -D-
HTTP/2 404
server: istio-envoy
date: Fri, 24 Mar 2023 04:13:12 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 1
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
# また、ServiceEntryで指定していない、Serviceのホスト名を指定してリクエストを飛ばしても同じようにDirectResponseが有効となる。
bash-4.4$ curl nginx:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 03:35:01 GMT
server: direct-responser
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
#
# ただし、virtual_hosts.domainsに指定していないドメインを指定してリクエストを送付すると、ループが発生してエラーになる。
# ループが発生しているためupstream requestsのメトリクスがすごい数インクリメントされている
# またHTTPレスポンスは返ってきていない。
# myhost:~$ oc get svc nginx
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# nginx ClusterIP 172.30.52.57 <none> 8080/TCP,8443/TCP 9d
bash-4.4$ curl 172.30.52.57:8080/-/direct -D-
HTTP/1.1 503 Service Unavailable
content-length: 91
content-type: text/plain
date: Fri, 24 Mar 2023 04:19:28 GMT
server: direct-responser
x-envoy-upstream-service-time: 53873
upstream connect error or disconnect/reset before headers. reset reason: connection failure
bash-4.4$
また、サーバー側のEnvoyで今回設定しているので、envoy同士の通信でなくてもDirectResponseが有効になっていることが確認できます。
# sidecar自身からリクエストをするとEnvoyを経由しなくなることの確認
# ServiceMeshの中でだけ有効なドメインは使えない。
myhost:~$ oc exec -ti -c istio-proxy nginx-64d4474bf5-vdn2c-debug -- curl nginx.test.direct:8080/-/direct -D-
curl: (6) Could not resolve host: nginx.test.direct
command terminated with exit code 6
# sidecar自身からリクエストを飛ばし、それでもDirectResponseが有効なことを確認
myhost:~$ oc exec -ti -c istio-proxy nginx-64d4474bf5-vdn2c-debug -- curl nginx:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 04:25:53 GMT
server: direct-responser
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
myhost:~$
各リクエストが確かにサーバー側に届いていることは、Envoyのアクセスログを見ると一目瞭然です。
上に上げていたように、virtual_hosts.domainsに存在しないドメインを指定してサーバーにリクエストが到着した際はループしてしまって接続不可となることも確認できました。この問題を回避するため、Inbound通信専用のROUTE_CONFIGURATIONを作成して指定してあげるという方法が考えられます。
2例目:実用できる実装例
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: direct-response-403-route2
labels:
self.test: "true"
spec:
workloadSelector:
labels:
deployment: nginx
configPatches:
- applyTo: NETWORK_FILTER
match:
listener:
portNumber: 8080
filterChain:
filter:
name: envoy.filters.network.tcp_proxy
context: SIDECAR_INBOUND
patch:
operation: REPLACE
value:
name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: direct_responser_route
route_config: #<-*1
name: "direct_response"
virtual_hosts:
- name: "nginx.direct_response:8080"
include_request_attempt_count: true
domains: #<-*2
- "*"
routes:
- name: direct_response
match:
prefix: "/-/direct"
direct_response:
status: 403
body:
inline_string: |
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
- name: allow_any
match:
prefix: "/"
route:
cluster: "inbound|8080||"
timeout: 0s
max_stream_duration:
max_stream_duration: 0s
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
server_name: direct-responser
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"
path: /dev/stdout
log_format:
text_format_source:
inline_string: "[%START_TIME%] \"%REQ(:authority)%\" direct_response_ptn2: passed replaced NWfilter\n"
- *1) route_config -> 今まで用いていたRDSにおいて参照する
envoy.config.route.v3.RouteConfiguration
では、Istioの制約により新しくエントリを追加することができないため、http_connection_managerのフィールドとして定義 - *2) virtual_hosts.domains -> 専用のRouteConfigurationを作成したため、その他のOUTBOUND通信に影響を与える心配がなくなり、ワイルドカードドメインが指定できる。これにより1例目の自分宛のドメイン全て指定する必要があるという問題を回避
以上の実装によって、1例目ではできなかった以下のリクエストや、Pod IPに直接来るようなリクエストに対してもDirectResponseを有効化し、サーバー側を保護するといったことが可能になります。
# myhost:~$ oc get svc nginx
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# nginx ClusterIP 172.30.52.57 <none> 8080/TCP,8443/TCP 9d
# myhost:~$
# Service向けの通信が通るようになる
bash-4.4$ curl 172.30.52.57:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 05:08:42 GMT
server: direct-responser
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
# myhost:~$ oc get po -o wide -l deployment=nginx
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# nginx-64d4474bf5-vdn2c 2/2 Running 0 9d 10.131.0.30 ip-10-0-200-55.ec2.internal <none> <none>
# myhost:~$
# PodのIPに直接通信しても通る
bash-4.4$ curl 10.131.0.30:8080/-/direct -D-
HTTP/1.1 403 Forbidden
content-length: 201
content-type: text/plain
date: Fri, 24 Mar 2023 05:09:02 GMT
server: envoy
x-envoy-upstream-service-time: 0
{
"msg":"this response directory generated by config.route.v3.Route (ptn2)",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-route"
}
bash-4.4$
# もちろんそれ以外のパスへの通信も通る
# VirtualServiceも関係ないので、全てのリクエストはサーバー側まで届いている状態
bash-4.4$ curl 10.131.0.30:8080/ -D-
HTTP/1.1 200 OK
server: envoy
date: Fri, 24 Mar 2023 05:13:21 GMT
content-type: text/html
content-length: 60
last-modified: Sat, 28 May 2022 22:07:12 GMT
etag: "62929d10-3c"
accept-ranges: bytes
x-envoy-upstream-service-time: 1
<html>
<body>
<div>Hello from Nginx</div>
</body>
</html>
bash-4.4$
以上で、HTTP Routeの config.route.v3.DirectResponseActionというフィールドによる実装例を見てきました。
ex)3. Network Filterでの実装例
最後にTCPレベルで実装する例について挙げて見たいと思います。
今まではHTTPリクエスト専用に色々用意されていた機能を使っていたのですが、TCPレベルだと利用出来なくなります。一番困るのが、URIによる振り分けができないため、特定のエンドポイントとポートの組み合わせに対してDirect Responseしか返せなくなることです。
用いれるMatch条件はこちらに記載されていますが、無論Transport Layerでの情報しか使えないようになっています。
設定イメージ
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: direct-response-403-tcp
spec:
workloadSelector:
labels:
deployment: nginx
configPatches:
- applyTo: LISTENER
match:
context: SIDECAR_INBOUND #<-*1
patch:
operation: ADD
value:
name: direct-responser
address: #<-*2
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filters:
- name: envoy.filters.network.direct_response
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.direct_response.v3.Config"
response: #<-*3
inline_string: |-
HTTP/1.1 403
server: istio-direct
content-length: 275
{
"msg":"this response directory generated by extensions.filters.network.direct_response.v3.Config",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/direct_response/v3/config.proto#extensions-filters-network-direct-response-v3-config"
}
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"
path: /dev/stdout
log_format:
text_format_source:
inline_string: "[%START_TIME%] \"%REQ(:authority)%\" direct_response_ptn3: passed Listener\n"
- *1) match.context=SIDECAR_INBOUND -> LISTENERおよびNetwork Filterへの設定のため、contextはINBOUND
- *2) address -> 今回SNI等ではなくポート単位で設定するため、Listenerを新たに定義し、新規に作成したListenerに対する通信に対してDirect Responseを適用させる
- *3) response -> TCPレベルでレスポンスを定義するため、HTTPに沿って平文で内容を記載する。
このように設定すると、たとえ元のサーバーが8443でListenしていなくてもEnvoyによってあたかもそのportでListenしているサーバーがあるかのように定義できる。
実際の挙動は以下のとおりである。
#bash-4.4$ uname -n
#nginx-64d4474bf5-vdn2c-debug
# 8080番ポートへの通信は元のサーバーに到達する。
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 404 Not Found
server: envoy
Date: Fri, 24 Mar 2023 06:43:41 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 4
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
# Envoy(IngressGateway)を通した時も同様で、8080ポートへ行っている限りDirectResponseは有効でない
bash-4.4$ curl https://nginx.test.direct.external/-/direct -D- -k
HTTP/2 404
server: istio-envoy
date: Fri, 24 Mar 2023 06:56:24 GMT
content-type: text/html
content-length: 153
x-envoy-upstream-service-time: 5
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
# ドメインやリクエスト内容に関係なく、8443番ポートへの通信はdirect responseが返る
bash-4.4$ curl nginx:8443/ -D-
HTTP/1.1 403
server: istio-direct
content-length: 275
{
"msg":"this response directory generated by extensions.filters.network.direct_response.v3.Config",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/direct_response/v3/config.proto#extensions-filters-network-direct-response-v3-config"
}
bash-4.4$
一見役に立たなさそうではあるが、例えばVirtualServiceなんかで8443番にリダイレクトさせてあげるとHTTPの条件がそのまま適用しやすくなる。
VirtualServiceを更新
※何度か言及しているようにEnvoyFilterではmatchさせる順番が重要で、今回catch-allな定義より前に変更を加えてあげる必要がある
myhost:~$ oc diff -f vs-tcp.yaml
diff -u -N /tmp/LIVE-239916318/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-external /tmp/MERGED-872475621/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-external
--- /tmp/LIVE-239916318/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-external 2023-03-24 16:02:09.872728100 +0900
+++ /tmp/MERGED-872475621/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-external 2023-03-24 16:02:10.082728100 +0900
@@ -5,7 +5,7 @@
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"labels":{"self.test":"true"},"name":"directresponse-external","namespace":"test-istio-01"},"spec":{"gateways":["directresponse"],
"hosts":["nginx.test.direct.external"],"http":[{"match":[{"port":443,"uri":{"prefix":"/-/"}}],"route":[{"destination":{"host":"nginx","port":{"number":8080}}}]}]}}
creationTimestamp: "2023-03-20T02:34:06Z"
- generation: 1
+ generation: 2
labels:
self.test: "true"
managedFields:
@@ -40,6 +40,15 @@
- match:
- port: 443
uri:
+ exact: /-/direct
+ route:
+ - destination:
+ host: nginx
+ port:
+ number: 8443
+ - match:
+ - port: 443
+ uri:
prefix: /-/
route:
- destination:
diff -u -N /tmp/LIVE-239916318/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-internal /tmp/MERGED-872475621/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-internal
--- /tmp/LIVE-239916318/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-internal 2023-03-24 16:02:09.442728100 +0900
+++ /tmp/MERGED-872475621/networking.istio.io.v1beta1.VirtualService.test-istio-01.directresponse-internal 2023-03-24 16:02:09.662728100 +0900
@@ -5,7 +5,7 @@
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"labels":{"self.test":"true"},"name":"directresponse-internal","namespace":"test-istio-01"},"spec":{"gateways":["mesh"],"hosts":["nginx.test.direct"],"http":[{"match":[{"port":8080,"uri":{"prefix":"/-/"}}],"route":[{"destination":{"host":"nginx","port":{"number":8080}}}]}]}}
creationTimestamp: "2023-03-20T02:33:39Z"
- generation: 1
+ generation: 2
labels:
self.test: "true"
managedFields:
@@ -40,6 +40,15 @@
- match:
- port: 8080
uri:
+ exact: /-/direct
+ route:
+ - destination:
+ host: nginx
+ port:
+ number: 8443
+ - match:
+ - port: 8080
+ uri:
prefix: /-/
route:
- destination:
myhost:~$
myhost:~$ oc apply -f vs-tcp.yaml
virtualservice.networking.istio.io/directresponse-internal configured
virtualservice.networking.istio.io/directresponse-external configured
myhost:~$
再度挙動確認
# 元の宛先
# Direct Responseが返ってくるようになっていることが分かる
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 403 Forbidden
server: envoy
content-length: 275
x-envoy-upstream-service-time: 4
date: Fri, 24 Mar 2023 07:05:58 GMT
{
"msg":"this response directory generated by extensions.filters.network.direct_response.v3.Config",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/direct_response/v3/config.proto#extensions-filters-network-direct-response-v3-config"
# Envoy(IngressGateway)を介した場合
# こちらもDirect Responseが返ってくるようになっていることが分かる
bash-4.4$ curl https://nginx.test.direct.external/-/direct -D- -k
HTTP/2 403
server: istio-envoy
content-length: 275
x-envoy-upstream-service-time: 2
date: Fri, 24 Mar 2023 07:06:16 GMT
{
"msg":"this response directory generated by extensions.filters.network.direct_response.v3.Config",
"url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/direct_response/v3/config.proto#extensions-filters-network-direct-response-v3-config"
}
bash-4.4$
# VirtualServiceに定義していない宛先
# こちらはもちろんDirect Responseは有効にならない
bash-4.4$ curl nginx:8080/-/direct -D-
HTTP/1.1 404 Not Found
Server: nginx/1.18.0
Date: Fri, 24 Mar 2023 07:08:50 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
bash-4.4$
もちろんTCPレベルの実装になるので、管理の手間はあるがHTTP以外のプロトコルも可能です。base64 encodeすればbyte文字列も送ることができるので、HTTP/2とかgRPCとかもレスポンスすることが可能です。
response:
- inline_string: |-
- HTTP/1.1 403
- server: istio-direct
- content-length: 275
-
- {
- "msg":"this response directory generated by extensions.filters.network.direct_response.v3.Config",
- "url":"https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/direct_response/v3/config.proto#extensions-filters-network-direct-response-v3-config"
- }
+ inline_bytes: SFRUUC8xLjEgNDAzCnNlcnZlcjogaXN0aW8tZGlyZWN0CmNvbnRlbnQtbGVuZ3RoOiA5NwoKewogIm1zZyI6InNlbnQgaW4gYnl0ZXMgc3R5bGUiLAogInVybCI6Imh0dHBzOi8vcHJvdG9idWYuZGV2L3Byb2dyYW1taW5nLWd1aWRlcy9wcm90bzMvI3NjYWxhciIKfQo=
name: direct-responser
bash-4.4$ curl nginx.test.direct:8080/-/direct -D-
HTTP/1.1 403 Forbidden
server: envoy
content-length: 97
x-envoy-upstream-service-time: 0
date: Fri, 24 Mar 2023 07:43:57 GMT
{
"msg":"sent in bytes style",
"url":"https://protobuf.dev/programming-guides/proto3/#scalar"
}bash-4.4$
複雑な分、色々組み合わせて工夫する余地があることが分かります。
終わりに
ServiceMeshは機能が多く複雑で、扱いに困る場面も多々あるかと思います。
但し元々は、ただでさえ複雑なマイクロサービスの苦労を取り除くために開発された機能なので、EnvoyFilterを覗くことを通してEnvoyと仲良くなれば実装に戸惑うことも少なくなるのではないかと思います。
筆者もまだまだ未知数なことが多いですが、分散アーキテクチャの世界に入門するため頑張って仲良くなりたいと思います。