SWEet

A Software Engineer Is Eating Technologies

Kubernetes The Hard Way におけるNodeを跨いだPod間のパケットフロー

何をするのか

github.com

Kubernetesの構築は各種クラウドベンダによるサービスによって非常に簡易化されている. 自前のサーバにKubernetesをデプロイする場合もkubeadmのようなツールによって必要なバイナリを全て手でインストールするというような手間を取る必要はなくなってきている.

しかし, 上記のリポジトリではGCEで用意したUbuntuインスタンスに全て手動でKubernetesコンポーネントをインストールしていくというハンズオンを体験できる.

今回はそのハンズオンで構築したKubernetes上で実際にPodからPodへ向けてのパケットフローがどのように行われるかを検証した.

環境

検証の環境は以下の画像のようになる. 厳密には3台のマスタに3台のワーカがGCE上で動作しているが今回検証で使用したのは2つのノードだけである. f:id:kk_river108:20200220122814p:plain

ストーリー

今回は以下のストーリーにおけるパケットの転送の様子を見てみる

pod 1からpod 2へpingする

pod 1からpod 2へpingする

この時, Node 1上でcnio0 ネットワークインタフェース(NI)をtcpdumpすると以下のような出力になる

03:45:45.813652 IP 10.200.0.2 > 10.200.1.2: ICMP echo request, id 2816, seq 0, length 64
03:45:45.815508 IP 10.200.1.2 > 10.200.0.2: ICMP echo reply, id 2816, seq 0, length 64
03:45:46.814361 IP 10.200.0.2 > 10.200.1.2: ICMP echo request, id 2816, seq 1, length 64
03:45:46.814807 IP 10.200.1.2 > 10.200.0.2: ICMP echo reply, id 2816, seq 1, length 64
03:45:47.814526 IP 10.200.0.2 > 10.200.1.2: ICMP echo request, id 2816, seq 2, length 64
03:45:47.814916 IP 10.200.1.2 > 10.200.0.2: ICMP echo reply, id 2816, seq 2, length 64

Podに割り当てられたvethからcnio0に対してパケットが転送されていることがわかる.

次にcnio0からNode本来のNIであるens4にパケットが転送されているかを確認したところ以下のような出力になっていた

03:49:47.934135 IP worker-0.us-west1-c.c.xxxx.internal > 10.200.1.2: ICMP echo request, id 5376, seq 0, length 64
03:49:47.935690 IP 10.200.1.2 > worker-0.us-west1-c.c.xxxx.internal: ICMP echo reply, id 5376, seq 0, length 64

ドメイン名をnslookupするとNodeのアドレスにNATされていることがわかる

$ nslookup worker-2.us-west1-c.c.xxxx.internal
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
Name:   worker-2.us-west1-c.c.xxxx.internal
Address: 10.240.0.20

ここまでの状況をまとめると以下の画像のようになる f:id:kk_river108:20200220132428p:plain

NodeのNIまでたどり着いたパケットは宛先が 10.200.0.0/24 に該当しないのでDefault Gatewayに転送する. この時のGWルータはGCE上で作成したVPCルータとなる.

kubernetes-the-hard-wayによる手順ではVPCルータに以下のルールを追加している

NAME                            NETWORK                  DEST_RANGE     NEXT_HOP                  PRIORITY
default-route-081879136902de56  kubernetes-the-hard-way  10.240.0.0/24  kubernetes-the-hard-way   1000
default-route-55199a5aa126d7aa  kubernetes-the-hard-way  0.0.0.0/0      default-internet-gateway  1000
kubernetes-route-10-200-0-0-24  kubernetes-the-hard-way  10.200.0.0/24  10.240.0.20               1000
kubernetes-route-10-200-1-0-24  kubernetes-the-hard-way  10.200.1.0/24  10.240.0.21               1000
kubernetes-route-10-200-2-0-24  kubernetes-the-hard-way  10.200.2.0/24  10.240.0.22               1000

これによって宛先10.200.1.2のパケットは 10.240.21に転送される.

ここまでの処理でパケットはNode 2のens4 NIに到達した f:id:kk_river108:20200220132349p:plain

Node2でのrouteは以下のようになっているのでcnio0に転送される. cnio0にはvethが接続されており, あとはpodのvethに転送された後, ペアのeth0に送信され,往路のパケット転送が完了する.

route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 ens4
10.200.1.0      0.0.0.0         255.255.255.0   U     0      0        0 cnio0
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 ens4

復路もVPCルータを介してNode 1に届くまでは一緒だが, Pod 2から送出されるパケットの宛先IPは Pod 1のIPアドレスではなく, Node 1のens4のIPアドレスとなっている. これは下記のiptablesのルールによってSNATされているためである.

Chain POSTROUTING (policy ACCEPT 7 packets, 696 bytes)
 pkts bytes target     prot opt in     out     source               destination
47471 4663K KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
   97  7561 CNI-be6576a86b4e6dd707f727f8  all  --  *      *       10.200.2.2           0.0.0.0/0            /* name: "bridge" id: "6f7cc23d7330f226cf75461157038a8718c6bb9b03a53a4bdfd7c1388d49fcfa" */
    0     0 CNI-014fd6fdf5198c5b6219910e  all  --  *      *       10.200.2.3           0.0.0.0/0            /* name: "bridge" id: "0c54384b9da08224e9733348a2ef7da105c285ebd50c62a0c30cebf0712e5e55" */

Chain CNI-be6576a86b4e6dd707f727f8 (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            10.200.2.0/24        /* name: "bridge" id: "6f7cc23d7330f226cf75461157038a8718c6bb9b03a53a4bdfd7c1388d49fcfa" */
   97  7561 MASQUERADE  all  --  *      *       0.0.0.0/0           !224.0.0.0/4          /* name: "bridge" id: "6f7cc23d7330f226cf75461157038a8718c6bb9b03a53a4bdfd7c1388d49fcfa" */

VPCルータからNode 1に届けられたリプライパケットはPod1からリクエストされた際にiptablesで適用されたPOSTROUTINGテーブルのIPマスカレードのルールが icmpパケット内のシーケンスIDからPod1のIPへ逆NATする.

POSTROUTINGの返信における適用: unix.stackexchange.com

これによって宛先IPはPod1の 10.200.0.2/32 となることでcnio0にフォワーディングされ, cnio0がもつ内部テーブルから宛先のvethへパケットが転送され, Pod 1へ正しくルーティングされる.

まとめ

今回のハンズオンで実験した環境はdockerのネットワークと同等である.

=> マルチホストでのDocker Container間通信 第1回: Dockerネットワークの基礎 - UZABASE Tech Blog

そのためflannelのようなCNIにおけるvxlanによるパケットのラップやvtepによるMAC addressの解決は起こっていない. 基本的にはNodeのiptablesにおけるNATのみでNodeをまたいだpod間の通信は行われていることがわかった.

謝辞

今回疑問の解決に協力していただいたTwitterのフォロワーの方やKubernetes Slackの方々にこの場を借りてお礼申し上げます.