Kubernetesの通信障害をtcpdumpで解決する

KubernetesとロードバランサーやAPIのエンドポイントなどの外部のデバイスと通信がうまくいっていなかったり、途中で切れたりしたときにtcpdumpを使ってパケットを解析すると、早期の問題解決につながります。Kubernetes環境でないVMでは、VM上でtcpdumpを実行すれば簡単にパケットをとれますが、Kubernetesの場合は少し工夫が必要なのでそのあたりをまとめてみました。

# 準備
tcpdumpを実行すると、フィルターを使用しても大量のデータを取得してしまいます。時によっては意図せずに、コンプライアンス上やセキュリティー上とるべきでないパケットなんかも見れてしまいます。可能であればテスト環境で問題の再現を行って、テストデータをtcpdumpで取得することをお勧めします。また実際にそのほうが、余分なパケットが少なく必要なデータだけを取得しやすいので解析時間も短くなるはずです。

先ずは複数あるノードから１つtcpdumpを実行するノードを選択し、そのノードに問題が発生しているアプリケーションをデプロイします。

ノード`myk8snode2`を使用するので、`myk8snode0`、`myk8snode1`をcordonします。
```bash
kubectl get no
NAME         STATUS   ROLES    AGE   VERSION
myk8snode0   Ready    <none>   10d   v1.24.4
myk8snode1   Ready    <none>   10d   v1.24.4
myk8snode2   Ready    <none>   10d   v1.24.4

kubectl cordon myk8snode0 myk8snode1
```

もしくは`nodeAffinity`を使って指定のノードにPodをデプロイします。
```yaml
nodeAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
      - key: kubernetes.io/hostname
        operator: In
        values:
        - myk8snode2
```
確認すると、Pod`myapppod-1bw5d-1234`が`myk8snode2`にデプロイされているのがわかります。
```yaml
kubectl -n myapp get pod -o wide
NAME                  READY   STATUS    RESTARTS        AGE    IP             NODE         NOMINATED NODE   READINESS GATES
myapppod-1bw5d-1234   1/1     Running   1 (1d2h ago)    4d1h   100.111.2.111  myk8snode2   <none>           <none>
```

次にtcpdumpを実行するためのPodをデプロイします。
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: nsenter
  namespace: nsenter
spec:
  hostPID: true
  hostNetwork: true
  hostIPC: true
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - myk8snode2
  containers:
    - name: nsenter
      image: ubuntu
      command: ['tail', '-f', '/dev/null']
      securityContext:
        privileged: true

```
ノード上すべてのPodの通信を取得したいので、管理者権限などが付いたPodです。トラブルシューティング以外の時は使わないほうがいいと思います。

デプロイしたPodにexecします。

```bash
kubectl -n nsenter exec -it nsenter -- nsenter --target 1 --mount --uts --ipc --net /bin/bash
```
これで、Pod`nsenter`から`myk8snode2`内の通信を見ることができます。Pod内で
```bash
ip link
```
とかするとノード上の全てのPodのインターフェースを確認できます。

tcpdumpで特定のPodだけのパケットを取得したい場合は、Podのインターフェースを指定する必要があります。
Pod`myapppod-1bw5d-1234`のインターフェースは、以下のように確認できます。
```bash
kubectl exec myapppod-1bw5d-1234 -it -- cat /sys/class/net/eth0/iflink
32
```
これで`nsenter`Podからip linkで32を探すと、こんな感じで確認できます。
```bash
ip link|grep 32
32: veth32d4edf@if4: .........
```
ノード`myk8snode2`上では`veth32d4edf`がPod`myapppod-1bw5d-1234`のインターフェースとなります。

# tcpdumpの実行
Pod`nsenter`からtcpdumpを実行します。

先ほどのPod`myapppod-1bw5d-1234`のパケットのみを取得する場合は、
```bash
tcpdump -v -XX -tt -i veth32d4edf port 443 -w /dump/tcpdump.pcap
```
のような感じで実行すると、Pod`myapppod-1bw5d-1234`のport 443のパケットを全て取得できます。フィルターを使って、解析に必要な最小限のパケットを取得します。

また、どのPodが問題を起こしているの分からないときや、`nginx`などを使ってサービスを公開して外部と通信している場合に通信の全体を見るには
```bash
tcpdump -v -XX -tt -i any port 443 -w /dump/tcpdump.pcap
```
とするとノード`myk8snode2`上のすべてのPodのおよび外部との通信のport 443のパケットを取得できます。


# 解析
先ほど取得したtcpdumpをWiresharkなんかを使って解析します。

ノード上のすべてのインターフェースから取得すると、例えば以下のようなIPを通ったパケットを見ることができます。
* 外部デバイスのIP
* ノードのIP
* ロードバランサーのIP
* `nginx` Ingress controllerのIP
* バックエンドサービスのPodのIP

例えば外部からのリクエストが、`nginx`を通してバックエンドのサービスのPod`myapppod-1bw5d-1234`にどのように送信され、どのIPからRSTを出しているかなどがわかります。これによって通信の遮断の原因がアプリケーションなのか、ロードバランサーなのかもしくは外部のクライアントなのかがわかります。

```bash
No.	Time	Source		    Destination	 Protocol Length TTL	Info
349	4.8666　100.111.2.111	10.23.45.12	 TCP      56	 63	    58780 → 443 [RST] Seq=98 Win=0 Len=0
```
この例だとPod`myapppod-1bw5d-1234`のIP`100.111.2.111`が、外部のエンドポイント`10.23.45.12`にRSTのパケットを送っています。何らかの原因で、Podとエンドポイントとの通信が正常に終了せず中断されています。考えられる原因としては、タイムアウトなど時間がかかりすぎて中断しているなどあります。

使用しているPodでタイムアウトなどのエラーをしっかりログで出してくれれば、ログ上でエラーを確認できるのでここまでしてパケットの中を見る必要もないです。しかし、すべてのアプリケーションでログが期待できるわけでもないので、ログで分からない場合はこのような方法で通信障害を解析することもできます。