tcpdumpでistioをデバッグして遊ぶ

kubernetes の周辺で service mesh の話が聞かれるようになってそこそこ経ちました。istio に関して言えば、今年の半ばに1.0もリリースされています。今後 kubernetes と istio という組み合わせはどんどん一般的なものになっていきそうです。

例えば istio には、特定条件下のリクエストの振り分け、リトライ、gRPC の Client loadbalancing をしてくれる機能があります。それぞれ至って簡単なアイディアですが、それらに機能を実現するために istio 内部では複数のコンポーネントが複雑に連携しています。そういった内部的なアーキテクチャを知りたければ、例えば traffic-management のページにいくと詳しい解説を見ることができます。ただ、やはりドキュメントにない細かい挙動を知りたい、実際に動かしながら挙動を見てみたほうがわかりやすい、ドキュメントにはそう書いてあるがどう考えてもそうやって動いているようなようにみえない、といったこともあると思います。

そういった際にはデバック的な方法を取ることになります。そのうちの一つとして tcpdump が挙げられています。

ですがこのページには以下のような簡単な記述しかありません。

With Tcpdump

Tcpdump doesn’t work in the sidecar pod - the container doesn’t run as root. However any other container in the same pod will see all the packets, since the network namespace is shared. iptables will also see the pod-wide configuration.

Communication between Envoy and the app happens on 127.0.0.1, and is not encrypted.

最初見たとき、自分は具体的にどういった手順を踏めばいいのかよくわかりませんでした。落ち着いて考えてみればすごく難しいわけではないのですが、同じように感じる人がいるかもしれないのでここに具体的な手順を書いていきたいと思います。また、取ってきたパケットを覗いて少しだけ考察もしてみたいと思います。

環境はGKEを使った kubernetes cluster を想定しますが、一部読み替えれば応用はできるはずです。

bookinfoを作る

kubernetes cluster の作成と istio のインストールは既に済んでいるものとします。

今回はサンプルとして bookinfo を使います。基本的には以下のページの通り設定していけば良いはずですが、ある程度 node のリソースが必要な点は気をつけましょう。

以下に自分の方で実行したコマンドだけ列挙しますが、少し公式のものに手を加えているので注意してください。具体的には default namespace を使わず istio-injection=enabled になっている新しい namespace を作っています。また、上記リンクの内容だけでは istio の機能を大して使えず面白くなさそうだったので、 virtual-service-all-v1.yaml 及び virtual-service-reviews-test-v2.yaml を追加で適用しています。

$ git clone -b release-1.0 https://github.com/istio/istio /hoge/istio-1.0.0 && cd /hoge/istio-1.0.0
$ kubectl create namespace bookinfo
$ kubectl label namespace bookinfo istio-injection=enabled
$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml -n bookinfo

上記実行後、こちら に従ってingress ip と port を調べます。

ブラウザで http://$GATEWAY_URL/productpage に行けば bookinfo のページが見れると思います。また、jason というユーザーを作ってログインすると通常のユーザーとは少し違うUIになっているはずです。具体的には Book Reviews の欄に rating の星が見えていると思います。

tcpdumpの設置と実行をする

bookinfo の作成ができたら、次に tcpdump の設置を行います。今回は bookinfo の gateway 的な立ち位置になっている productpage に corfr/tcpdump を横付けします。

pod-overview のページに解説がありますが、Pod には container を複数指定することができ、その場合でもネットワーク等は container 間で共有されます。つまり、任意の Pod に対して tcpdump の container を追加し実行すれば、一方の container で起きたトラフィックでも tcpdump の container から見えるというわけです。

bookinfo を作成する際に使った yaml ファイルを以下のように修正し、もう一度 kubectl apply します。面倒であれば kubectl edit して変えても良いと思います。

diff --git a/samples/bookinfo/platform/kube/bookinfo.yaml b/samples/bookinfo/platform/kube/bookinfo.yaml
index c0470c400..0b2281e9c 100644
--- a/samples/bookinfo/platform/kube/bookinfo.yaml
+++ b/samples/bookinfo/platform/kube/bookinfo.yaml
@@ -189,4 +189,7 @@ spec:
         imagePullPolicy: IfNotPresent
         ports:
         - containerPort: 9080
+      - name: tcpdump
+        image: corfr/tcpdump
+        command: ["sleep", "10000"]
 ---

ここでは ["sleep", "10000"] としていますが、例えばここは以下のようにし、この /var/productpage.pcap の内容を Persistent Volume に書き出したりする方法もあるかもしれません。

- name: tcpdump
  image: corfr/tcpdump
  args: ["-i", "eth0", "-A", "-w", "/var/productpage.pcap"]

ただ、Persistent volume の準備が若干手間なのと、デバッグ用途であることから、今回はお手軽にコンテナに接続して直接 tcpdump を実行する方法をとることにしました。

実行は以下のように行います。以下を実行中、ブラウザから bookinfo にアクセスします。いくつかリクエストを送ったら tcpdump を終了します。

$ kubectl exec -it productpage-v1-5869b65c77-59j8r -n bookinfo -c tcpdump -- tcpdump -i eth0 -A -w /var/productpage.pcap
$ kubectl exec -it istio-ingressgateway-58f5f7644d-blmbs -n istio-system -c tcpdump -- tcpdump -i eth0 -A -w /var/gateway.pcap

tcpdumpの実行結果を手元に持ってくる

キャプチャした内容を手元に持ってくる方法ですが、今回はシンプルにssh(!)します。まずは先程の pod がどの node にあるのか確認します。

$ kubectl get pod productpage-v1-5869b65c77-59j8r -n bookinfo -o wide
NAME                              READY     STATUS    RESTARTS   AGE       IP            NODE
productpage-v1-5869b65c77-59j8r   3/3       Running   0          1m        10.48.40.84   gke-test-cluster-default-pool-d2e6f5a4-5nkn

今回はGKE環境を使っているので、以下のように ssh できます。

$ gcloud compute --project $PROJECT_ID ssh --zone $ZONE gke-test-cluster-default-pool-d2e6f5a4-5nkn

インスタンス上で docker ps をすればどれが tcpdump のプロセスなのかわかります。その後 docker cp でコンテナからGCEインスタンスに移します。

$ xxxx@gke-test-cluster-default-pool-d2e6f5a4-5nkn ~ $ docker ps | grep tcpdump
4d3feb7416e7   corfr/tcpdump@sha256:3006b3bd9f041bf73f21e626b97cca5e78fd6ce271549ca95b8e6a508165512b   "sleep 10000"    11 minutes ago    Up 11 minutes    k8s_tcpdump_productpage-v1-5869b65c77-z5szc_bookinfo_5e30a482-d698-11e8-9222-42010a9201a5_0

$ xxxx@gke-test-cluster-default-pool-d2e6f5a4-5nkn ~ $ docker cp 4d3feb7416e7:/var/productpage.pcap .

最後に gcloud scp すればキャプチャした内容を手元に持ってくることができます。

$ gcloud compute --project $PROJECT_ID scp --zone $ZONE gke-test-cluster-default-pool-d2e6f5a4-5nkn:/home/xxxx/productpage.pcap .

wiresharkの設定をする

tcpdump の内容は wireshark で見ることができます。 charles も使えそうですが、tcpdump の結果は wireshark の方が見やすいと思います。

キャプチャした内容を見るには、単純に上記の手順で取得したファイルを wireshark で開けばOKです。しかし、開いてみるとわかると思うのですが、そのままでは特に目立った情報は見られないと思います。これは Pod と istio のやりとりに gRPC が使われており、wireshark でHTTP2のみるには追加でデコード方法を指定する必要があるためです。

デコードの設定は、もしくは wireshark のメニューから Menu > Analyze > Decode As... と辿っていけば見つかるはずです(適当なパケットを右クリックし Decode As を選んでもよい)。

以下のスクリーンショットのように、二番目の列にポート番号、最後の行にHTTP2を指定します。

f:id:gong023:20181216105843p:plain

ポート番号ですが、istioctl を使えば productpage から見たときにどれがどのポートに見えるのか確認できます。このなかから istio に関するものをピックアップして指定するのが良いと思います。

$ istioctl proxy-config clusters productpage-v1-5869b65c77-z5njr -n bookinfo | grep istio

キャプチャした内容を少し考察してみる

ここまでの段階で、以下のような内容を得られています。

f:id:gong023:20181216105911p:plain

どのIPがどの pod なのかは、 kubectl get pods --all-namespaces -o wide とかすればわかるはずです。

681行目から /productpage のリクエストが始まります。687行目で productpage から istio-policy (mixerとかを司るところ)へ gRPC のリクエストが送られています。

gRPC はすべてPOSTメソッドで、パスが protocol buffer に対応します。従ってこの呼出しは以下の proto に対応しているようです。

https://github.com/istio/istio/tree/master/mixer に解説がありますが、このAPIDestination Rule に従った送信先見つけているのかなと思いました(コードをちゃんと読んだわけではないので間違っているかもしれません。あしからずお願いします)。

Precondition Checking.

Enables callers to verify a number of preconditions before responding to an incoming request from a service consumer. Preconditions can include whether the service consumer is properly authenticated, is on the service's whitelist, passes ACL checks, and more.

今回試した限り、他にも Report への呼び出しもみられました。色々みていて、意外と mixer のAPIの種類は少ないんだなあとか発見があり面白かったです。

他にやってみたいこと

今回の実験ですが、最初に述べたように istio のドキュメントに言及があったからというのがきっかけなのですが、How Istio manages microservice applications – A traffic flow analysis with TCP Capture というビデオをみて面白そうだなと思ったのもありました。

このビデオは自分がキャプチャした内容よりとれているAPI呼び出しが多かったです。一年以上前の内容なので istio の内部も色々と変わっていると思うのですが、もしかしたらどっかで何か見落としてるところがあるかもしれないと思っています。この記事で詳細は書きませんでしたが、実は自分はこのビデオ内容のように productpage と同時に istio-gateway のキャプチャもしていました。しかしあまり面白いものは見られずなんだろうなあと思っています。tcpdump の引数とかちゃんと見直してみてもいいかもしれません。また、今回は単純に /productpage をリロードしていただけですが、別の方法にしたら何かしら変化が見られたかもしれません。

また、今回 envoy からと思われるパケットがみられなかったのも気になっています。bookinfo は一般的なREST APIなので、gRPC のアプリケーションだったら何か変わるかなあとかも考えています。gRPC なら呼び出し元で loadbalancing がされるはずだし何かしら動きがありそうな気がします。pod の死活状態は streaming で送られるみたいな話をどっかで聞いたような気がするので今回の方法でできるかわかりませんが・・・。

というわけで、tcpdump を使う方法はなんとなくわかったものの、まだまだ istio についてはわからない部分も多いので気が向いたらまた何か試してみようと思います。

また、勘のいい人ならわかると思いますが、今回の方法は別に istio に関係なく使えます。単純に k8s 上にあるアプリケーションのデバッグ方法として覚えておくと役に立つこともあるかもしれません。