search
LoginSignup
14

More than 1 year has passed since last update.

posted at

updated at

GKE CNI Deep Dive (2021)

GKE (Google Kubernetes Engine) のネットワーク周りの実装はユーザーの見えないところで変化を続けています。以前は、公式ドキュメントにあるように bridge interface (cbr0) を介してホストマシン (ノード) とコンテナ間でパケットのやりとりが行われていました。しかし、コンテナランタイムを containerd に変更すると (もしくは、コンテナランタイムが dockershim でも比較的新しい時期に作成したクラスターなら?)、見慣れないコンポーネントや使用している CNI plugin の Network Configuration Lists (a.k.a. Chainning) が変わっていることに気付きます。

本記事では、以下の条件の GKE のネットワーク周りの実装について、一部推測も交えながら見ていきます。

Kubernetesネットワーク 徹底解説を更に深掘りしたような内容となっているため、先にそちらを読むと理解が進むかもしれないです。

TL;DR

  • GKE では netd と呼ばれる DaemonSet が各ノードの CNI の設定ファイルを作成
  • GKE で使用されている Main CNI plugingke
  • GKE CNI plugin は公式のリファレンス実装である PTP CNI plugin とほぼ同じ実装 (?)

netd

netd は、GKE Networking DaemonSet とあるように GKE の各ノードで動作しています。役割も README に書いてあるようにシンプルです。

netd is a Daemon designed to provide networking related features on GCP. The initial version is to generate CNI Spec for PTP plugin based on PodCIDR from Kubernetes API server.

❯ kubectl get ds -n kube-system
NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                                                             AGE
(...)
netd                        3         3         3       3            3           cloud.google.com/gke-netd-ready=true,kubernetes.io/os=linux               108d

netd は、init コンテナを使って install-cni.sh というシェルスクリプトを実行しています。ConfigMap 経由で環境変数をいくつか渡していますが、一部省略しています。また、hostPath を利用してホスト上の /etc/cni/net.d をマウントしている点も分かります。これだけで install-cni.sh が何をやっているか想像できる人もいるかもしれませんが、実際にシェルスクリプトの中を覗いてみましょう。

      initContainers:
      - command:
        - sh
        - /install-cni.sh
        env:
        - name: CNI_SPEC_TEMPLATE
          valueFrom:
            configMapKeyRef:
              key: cni_spec_template
              name: netd-config
        (...)
        image: gke.gcr.io/netd-amd64:v0.2.9-gke.0
    (...)
        volumeMounts:
        - mountPath: /host/etc/cni/net.d
          name: cni-net-dir
        - mountPath: /host/home/kubernetes/bin
          name: kubernetes-bin
          readOnly: true
      volumes:
      - hostPath:
          path: /etc/cni/net.d
          type: ""
        name: cni-net-dir
      - hostPath:
          path: /home/kubernetes/bin
          type: Directory
        name: kubernetes-bin

Google Cloud が管理しているコンポーネントは distroless のイメージを使っていることが多いですが、Makefile を覗くと netd は Alpine をベースイメージとして使用していることが分かります。ですので、ash を取得できます。init コンテナとメインのコンテナイメージが同じなので、メインのコンテナからも install-cni.sh の中身を見ることができます。

kubectl exec -it -n kube-system ds/netd -- ash

GitHub 上でも install-cni.sh を確認できますが、以下の差分があります。今回の話には影響がないので、ノードに SSH できない人は GitHub のソースコードを直接見てもらって構いません。

--- install-cni.sh  2021-10-10 22:06:09.000000000 +0900
+++ install-cni-github.sh   2021-10-10 22:06:36.000000000 +0900
@@ -121,16 +121,15 @@
   fi
 fi

-if [ "$ENABLE_PRIVATE_IPV6_ACCESS" == "true" ] || [ "$ENABLE_IPV6" == "true" ]; then
+if [ "$ENABLE_PRIVATE_IPV6_ACCESS" == "true" ]; then
   node_ipv6_addr=$(curl -s -k --fail "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/?recursive=true" -H "Metadata-Flavor: Google" | jq -r '.ipv6s[0]' ) ||:

   if [ -n "${node_ipv6_addr:-}" ] && [ "${node_ipv6_addr}" != "null" ]; then
     echo "Found nic0 IPv6 address ${node_ipv6_addr:-}. Filling IPv6 subnet and route..."
-
     cni_spec=$(echo ${cni_spec:-} | sed -e \
       "s#@ipv6SubnetOptional#, [{\"subnet\": \"${node_ipv6_addr:-}/112\"}]#g;
        s#@ipv6RouteOptional#, {\"dst\": \"::/0\"}#g")
-
+
     # Ensure the IPv6 firewall rules are as expected.
     # These rules mirror the IPv4 rules installed by kubernetes/cluster/gce/gci/configure-helper.sh
     if ip6tables -w -L FORWARD | grep "Chain FORWARD (policy DROP)" > /dev/null; then

GKE の Network Policy や IPv6 関連の機能を有効にしている場合に、処理が分岐していますが、以下の処理を行なっているだけです。

  • CNI の設定ファイルの生成
  • ローカルやプライベート IP レンジ以外の通信の送信元 IP をマスカレードする設定を追加

CNI の設定ファイルを作る処理の中で特に注目したいのが、@ipv4Subnet を置き換えている部分です。自身の Node オブジェクトの情報を取得するために curl を使って直接 kube-apiserver に問い合わせ、.spec.podCIDR を取得しています。(kubectl であれば JSONPath を使って特定のフィールドを取り出せますが、curl を使っているせいでわざわざ jq をイメージの中に入れている点は生々しくて良いですね。)

# https://github.com/GoogleCloudPlatform/netd/blob/v0.2.9/scripts/install-cni.sh#L78-L91
# Fill CNI spec template.
token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
node_url="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/nodes/${HOSTNAME}"
response=$(curl -k -s -H "Authorization: Bearer $token" $node_url)
ipv4_subnet=$(echo $response | jq '.spec.podCIDR')

if expr "${ipv4_subnet:-}" : '"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/[0-9][0-9]*"' >/dev/null; then
  echo "PodCIDR validation succeeded: ${ipv4_subnet:-}"
else
  echo "Response from $node_url"
  echo "$response"
  echo "Failed to fetch/validate PodCIDR from K8s API server, ipv4_subnet=${ipv4_subnet:-}. Exiting (1)..."
  exit 1
fi

つまり、Node が作られた時点でその Node にスケジュールされた Pod に割り当てる CIDR のレンジは決まっていて、IPAM (IP Address Management) plugin がその CIDR レンジから PodIP を払い出しているのです。

Node に対する PodCIDR の払い出しは Node IPAM Controller の役割です。Cloud Provider に依存した実装は out-of-tree での開発 (e.g. cloud-provider-gcp) に移行中ですが、まだ 移行できていないので legacy-cloud-providers の実装も併せて追いかけると良いと思います。

Range Allocator が起動時に割り当て済みの PodCIDR の情報を in-memory にマッピングし、Node の作成イベントをトリガーにその割り当て済みの PodCIDR の台帳を使って新たに割り当てる PodCIDR を決定します。実際に CIDR の払い出しを行なっている処理は以下です。

// https://github.com/kubernetes/kubernetes/blob/v1.20.9/pkg/controller/nodeipam/ipam/range_allocator.go#L245-L280
func (r *rangeAllocator) AllocateOrOccupyCIDR(node *v1.Node) error {
    (...)
    // allocate and queue the assignment
    allocated := nodeReservedCIDRs{
        nodeName:       node.Name,
        allocatedCIDRs: make([]*net.IPNet, len(r.cidrSets)),
    }

    for idx := range r.cidrSets {
        podCIDR, err := r.cidrSets[idx].AllocateNext()
        if err != nil {
            r.removeNodeFromProcessing(node.Name)
            nodeutil.RecordNodeStatusChange(r.recorder, node, "CIDRNotAvailable")
            return fmt.Errorf("failed to allocate cidr from cluster cidr at idx:%v: %v", idx, err)
        }
        allocated.allocatedCIDRs[idx] = podCIDR
    }

    //queue the assignment
    klog.V(4).Infof("Putting node %s with CIDR %v into the work queue", node.Name, allocated.allocatedCIDRs)
    r.nodeCIDRUpdateChannel <- allocated
    return nil
}

install_cni.sh のスクリプトに戻ります。PodCIDR が取得できたら、後は簡単で cni_spec のファイルを書き換えるだけです。

# https://github.com/GoogleCloudPlatform/netd/blob/v0.2.9/scripts/install-cni.sh#L93-L94
echo "Filling IPv4 subnet ${ipv4_subnet:-}"
cni_spec=$(echo ${cni_spec:-} | sed -e "s#@ipv4Subnet#[{\"subnet\": ${ipv4_subnet:-}}]#g")

cni_spec は以下の箇所で定義されています。GKE の Legacy Network PolicyCalico に依存しているので利用しているかどうかで CNI の設定ファイルのベースが分岐していたりします。

# https://github.com/GoogleCloudPlatform/netd/blob/v0.2.9/scripts/install-cni.sh#L41-L55
# Get CNI spec template if needed.
if [ "${ENABLE_CALICO_NETWORK_POLICY}" == "true" ]; then
  (...)
else
  cni_spec=${CNI_SPEC_TEMPLATE}
fi

CNI_SPEC_TEMPLATE の環境変数の値は env フィールドを使って ConfigMap 経由で渡されているので、ConfigMap の中を見ると分かります。

❯ kubectl get cm -n kube-system netd-config -o yaml
apiVersion: v1
data:
  cni_spec_name: 10-gke-ptp.conflist
  cni_spec_template: |-
    {
      "name": "gke-pod-network",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "@cniType",
          "mtu": @mtu,
          "ipam": {
              "type": "host-local",
              "ranges": [
              @ipv4Subnet@ipv6SubnetOptional
              ],
              "routes": [
                {"dst": "0.0.0.0/0"}@ipv6RouteOptional
              ]
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }@cniBandwidthPlugin@cniCiliumPlugin
      ]
    }
  (...)
kind: ConfigMap
metadata:
  name: netd-config
  namespace: kube-system

つまり、このテンプレートの中の @cniType, @mtu, @ipv4Subnet, @ipv6SubnetOptional, @cniBandwidthPlugin@cniCiliumPlugin をそれぞれ埋めて CNI の設定ファイルを作成しているようです。先ほど自身の Node オブジェクトから .spec.podCIDR を取ってきていましたが、これは host-local という公式リファレンス実装の IPAM plugin を使って PodIP として払い出す IP レンジを設定しています。

ちなみに、@cniType は以下の処理から決定されて埋め込まれます。

# https://github.com/GoogleCloudPlatform/netd/blob/v0.2.9/scripts/install-cni.sh#L57-L62
if [ -f "/host/home/kubernetes/bin/gke" ]
then
    cni_spec=$(echo ${cni_spec:-} | sed -e "s#@cniType#gke#g")
else
    cni_spec=$(echo ${cni_spec:-} | sed -e "s#@cniType#ptp#g")
fi

init コンテナは既に停止しているので、ホストマシンに SSH するなり、デバッグ用のコンテナを起動するなりして /home/kubernetes/bin/gke が存在するか確認します。今後の作業のことも考えて root 権限に切り替えておきます。

# sudo su -
# ls -la /home/kubernetes/bin/gke
-rwxr-xr-x 1 root root 4629262 Feb 18  2020 /home/kubernetes/bin/gke

gke のバイナリが存在するため、メインの plugin として gke が使われているようです。せっかくホストマシンに SSH したので、最終的に netd によって生成された CNI の設定ファイルを確認してみます。

# cat /etc/cni/net.d/10-gke-ptp.conflist
{ "name": "gke-pod-network", "cniVersion": "0.3.1", "plugins": [ { "type": "gke", "mtu": 1460, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "10.212.128.64/26"}] ], "routes": [ {"dst": "0.0.0.0/0"} ] } }, { "type": "portmap", "capabilities": { "portMappings": true } },{"type": "bandwidth","capabilities": {"bandwidth": true}} ] }

これを見ると、gke と host-local 以外にも portmapbandwidth plugin を利用していることが分かります。これらは全て /home/kubernetes/bin 以下に格納されています。

ちなみに、本当にこの CNI の設定が containerd に読み込まれているのかは、GKE のノードに pre-install されている crictl で確認できます。PluginDirs/home/kubernetes/bin になっており、PluginConfDir/etc/cni/net.d となっていることからも読み込まれていることが分かります。

# crictl info
{
  (...)
  "cniconfig": {
    "PluginDirs": [
      "/home/kubernetes/bin"
    ],
    "PluginConfDir": "/etc/cni/net.d",
    "PluginMaxConfNum": 1,
    "Prefix": "eth",
    "Networks": [
      (...)
      {
        "Config": {
          "Name": "gke-pod-network",
          "CNIVersion": "0.3.1",
          "Plugins": [
            {
              "Network": {
                "type": "gke",
                "ipam": {
                  "type": "host-local"
                },
                "dns": {}
              },
              "Source": "{\"ipam\":{\"ranges\":[[{\"subnet\":\"10.212.128.64/26\"}]],\"routes\":[{\"dst\":\"0.0.0.0/0\"}],\"type\":\"host-local\"},\"mtu\":1460,\"type\":\"gke\"}"
            },
            {
              "Network": {
                "type": "portmap",
                "capabilities": {
                  "portMappings": true
                },
                "ipam": {},
                "dns": {}
              },
              "Source": "{\"capabilities\":{\"portMappings\":true},\"type\":\"portmap\"}"
            },
            {
              "Network": {
                "type": "bandwidth",
                "capabilities": {
                  "bandwidth": true
                },
                "ipam": {},
                "dns": {}
              },
              "Source": "{\"capabilities\":{\"bandwidth\":true},\"type\":\"bandwidth\"}"
            }
          ],
          "Source": "{ \"name\": \"gke-pod-network\", \"cniVersion\": \"0.3.1\", \"plugins\": [ { \"type\": \"gke\", \"mtu\": 1460, \"ipam\": { \"type\": \"host-local\", \"ranges\": [ [{\"subnet\": \"10.212.128.64/26\"}] ], \"routes\": [ {\"dst\": \"0.0.0.0/0\"} ] } }, { \"type\": \"portmap\", \"capabilities\": { \"portMappings\": true } },{\"type\": \"bandwidth\",\"capabilities\": {\"bandwidth\": true}} ] }\n"
        },
        "IFName": "eth0"
      }
    ]
  },
  (...)
}

ここまで整理すると、

  • CNI の設定ファイルを netd が作成
  • GKE のメインの CNI plugin は gke

GKE CNI plugin

GKE CNI plugin の正体は一体何でしょうか。ここからは推測になるのですが、PTP CNI plugin をベースに一部修正したものである可能性が高いです。実際に gke のバイナリを実行してみると CNI ptp plugin と表示されます。ptp のバイナリを実行した結果と同じです。

# /home/kubernetes/bin/gke
CNI ptp plugin v0.8.5-gke.1

# /home/kubernetes/bin/ptp
CNI ptp plugin v0.8.5-gke.1

また、netd の README にも以下のように記載がありました。ここから更に深掘りを進めていきましょう。

The initial version is to generate CNI Spec for PTP plugin based on PodCIDR from Kubernetes API server.

まず始めに、この GKE CNI plugin が対応している CNI Spec を確認します。GKE CNI plugin に対して CNI Spec で定義されている Report version のコマンドを実行することで確認できます。

# echo '{}' | CNI_COMMAND=VERSION /home/kubernetes/bin/gke
{"cniVersion":"0.4.0","supportedVersions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0"]}

# # ptp も対応バージョンは同じ
# echo '{}' | CNI_COMMAND=VERSION /home/kubernetes/bin/ptp
{"cniVersion":"0.4.0","supportedVersions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0"]}

0.4.0 の CNI Spec まで対応していますが、/etc/cni/net.d/10-gke-ptp.conflist のCNI の設定ファイルで指定している 0.3.1 のバージョンの CNI Spec を利用しているようです。

次に、GKE CNI Plugin を使ってネットワークインターフェイスを作ってみましょう。どういう実装になっているのか分からないですが、CNI Spec に定義されている Add コマンドを実行して挙動を見ていきます。

Note: これ以降の手順は GKE のノードを破壊する可能性があるため、注意して下さい。

まずは、適当な network namespace を作成します。

# ip netns add helloworld

適当なコンテナを作成し、上記の network namespace に隔離して実行します。

# containerd --version
containerd github.com/containerd/containerd 1.4.3-0ubuntu0~20.04.1

今回は containerd に同梱されている ctr を使って操作します。

# ctr -n k8s.io images pull gcr.io/knative-samples/helloworld-go:latest
gcr.io/knative-samples/helloworld-go:latest:                                      resolved       |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:5ea96ba4b872685ff4ddb5cd8d1a97ec18c18fae79ee8df0d29f446c5efe5f50: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:6a297b1cfcdbab71363d6dc148cb48e86564814713b80eb343694be00885ff75:    done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:7de72db4aca729a7cc88116e5f8324942c3da2e54135860b421111c6f9a09540:   done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:bc9ab73e5b14b9fbd3687a4d8c1f1360533d6ee9ffc3f5ecc6630794b40257b7:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:193a6306c92af328dbd41bbbd3200a2c90802624cccfe5725223324428110d7f:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:e5c3f8c317dc30af45021092a3d76f16ba7aa1ee5f18fec742c84d4960818580:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:a587a86c9dcb9df6584180042becf21e36ecd8b460a761711227b4b06889a005:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:1bc310ac474b880a5e4aeec02e6423d1304d137f1a8990074cb3ac6386a0b654:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:87ab348d90cc687a586f07c0fd275335aee1e6e52c1995d1a7ac93fc901333bc:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:786bc4873ebcc5cc05c0ff26d6ee3f2b26ada535067d07fc98f3ddb0ef4cd7c5:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:bc6a2cf36a2e65403f9e46f67e5364764715fda127ba61083ce1cdff48ae9d7d:    done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 6.1 s                                                                    total:  284.1  (46.5 MiB/s)
unpacking linux/amd64 sha256:5ea96ba4b872685ff4ddb5cd8d1a97ec18c18fae79ee8df0d29f446c5efe5f50...
done

# # Create container attached to specific network namespace
# ctr -n k8s.io containers create gcr.io/knative-samples/helloworld-go:latest helloworld --with-ns network:/var/run/netns/helloworld

# # Start running container
# ctr -n k8s.io task start helloworld -d

# # Check if container is running as a process
# ctr -n k8s.io tasks ps helloworld
PID       INFO
703406    -

作成した network namespace に対して仮想ネットワークインターフェイスのペアを作成します。複数の CNI plugin を実行するため、Network configuration list runtime examples を参考に順番に実行していきます。

注意点として Network configuration  list の名前 (name フィールド) は CNI の設定ファイルと同じ gke-pod-network を指定するようにして下さい。

name (string): Network name. This should be unique across all containers on the host (or other administrative domain).
https://github.com/containernetworking/cni/blob/spec-v0.3.1/SPEC.md#network-configuration-lists

でないと、IPAM が既に払い出した IP を helloworld コンテナに割り当ててしまい、既存の動作している Pod IP のルーティングルールが上書きされて正常に動作しなくなります。

まずは、GKE CNI plugin を実行します。

# echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "gke", "mtu": 1460, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "10.212.128.64/26"}] ], "routes": [ {"dst": "0.0.0.0/0"} ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/gke
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "gke3a52ce78095",
            "mac": "9e:91:82:0c:89:c6"
        },
        {
            "name": "eth0",
            "mac": "46:63:4b:88:30:5e",
            "sandbox": "/var/run/netns/helloworld"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 1,
            "address": "10.212.128.82/26",
            "gateway": "10.212.128.65"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

10.212.128.82 の IP が IPAM から払い出されたようです。返却されたレスポンスの JSON を prevResult のフィールドに押し込んで portmap plugin を実行します。(prevResult の結果はノードによって異なるので注意して下さい。)

# echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "portmap", "capabilities": { "portMappings": true }, "prevResult": { "interfaces": [ { "name": "gke3a52ce78095", "mac": "9e:91:82:0c:89:c6" }, { "name": "eth0", "mac": "46:63:4b:88:30:5e", "sandbox": "/var/run/netns/helloworld" } ], "ips": [ { "version": "4", "interface": 1, "address": "10.212.128.82/26", "gateway": "10.212.128.65" } ], "routes": [ { "dst": "0.0.0.0/0" } ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/portmap
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "gke3a52ce78095",
            "mac": "9e:91:82:0c:89:c6"
        },
        {
            "name": "eth0",
            "mac": "46:63:4b:88:30:5e",
            "sandbox": "/var/run/netns/helloworld"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 1,
            "address": "10.212.128.82/26",
            "gateway": "10.212.128.65"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

GKE CNI plugin を実行した時と同じレスポンスが JSON で返ってきました。最後に prevResult のフィールドに同じ JSON を押し込んで bandwidth plugin を実行します。

# echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "bandwidth", "capabilities": { "bandwidth": true }, "prevResult": { "interfaces": [ { "name": "gke3a52ce78095", "mac": "9e:91:82:0c:89:c6" }, { "name": "eth0", "mac": "46:63:4b:88:30:5e", "sandbox": "/var/run/netns/helloworld" } ], "ips": [ { "version": "4", "interface": 1, "address": "10.212.128.82/26", "gateway": "10.212.128.65" } ], "routes": [ { "dst": "0.0.0.0/0" } ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/bandwidth
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "gke3a52ce78095",
            "mac": "9e:91:82:0c:89:c6"
        },
        {
            "name": "eth0",
            "mac": "46:63:4b:88:30:5e",
            "sandbox": "/var/run/netns/helloworld"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 1,
            "address": "10.212.128.82/26",
            "gateway": "10.212.128.65"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

これで、GKE で設定されている CNI plugin を実行することができました。仮想ネットワークインターフェイスのペアが作成され、片方がホストマシン上に、もう片方がコンテナの network namespace に繋がっているはずですので確認していきましょう。

まずは、ホストマシン上に繋がった仮想ネットワークインターフェイスを確認します。link-netns で helloworld の network namespace と紐づいていることが分かります。ルーティングテーブルにも払い出された IP がホスト上の仮想ネットワークインターフェイスを通過するルールが追加されていることが分かります。

# ip addr list gke3a52ce78095
6: gke3a52ce78095@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue state UP group default
    link/ether 9e:91:82:0c:89:c6 brd ff:ff:ff:ff:ff:ff link-netns helloworld
    inet 10.212.128.65/32 brd 10.212.128.65 scope global gke3a52ce78095
       valid_lft forever preferred_lft forever
    inet6 fe80::9c91:82ff:fe0c:89c6/64 scope link
       valid_lft forever preferred_lft forever

# ip route
default via 10.212.0.1 dev ens4 proto dhcp src 10.212.0.41 metric 100
10.212.0.1 dev ens4 proto dhcp scope link src 10.212.0.41 metric 100
(...)
10.212.128.82 dev gke3a52ce78095 scope host
169.254.123.0/24 dev docker0 proto kernel scope link src 169.254.123.1 linkdown

次に、helloworld コンテナの network namespace に入って仮想ネットワークインターフェイスのもう片方がどうなっているか確認します。確かに、eth0 の仮想ネットワークインターフェイスに 10.212.128.82 の IP が紐付いていることが分かります。また、ルーティングテーブルに GW (10.212.128.65) 以外への通信は全て ARP で問い合わせに行けというルールが追加されていることが分かります。

# ip netns exec helloworld ip addr list eth0
7: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue state UP group default
    link/ether 46:63:4b:88:30:5e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.212.128.82/26 brd 10.212.128.127 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4463:4bff:fe88:305e/64 scope link
       valid_lft forever preferred_lft forever

# ip netns exec helloworld ip route
default via 10.212.128.65 dev eth0
10.212.128.64/26 via 10.212.128.65 dev eth0 src 10.212.128.82
10.212.128.65 dev eth0 scope link src 10.212.128.82

ホストマシンから helloworld コンテナが動作している network namespace に対して疎通できるか確認してみましょう。GKE の Ubuntu イメージのノードには tcpdump や ping がインストールされておらず、デバッグ用のコンテナを起動するのも面倒なので、helloworld コンテナ (gcr.io/knative-samples/helloworld-go:latest) に対してリクエストを投げて確認します。helloworld コンテナで実行しているコードは helloworld-go にあり、8080 番ポートで Web サーバが動作しています。/ のパスに GET リクエストを投げると Hello World! を返します。

# curl -v http://10.212.128.82:8080/
*   Trying 10.212.128.82:8080...
* TCP_NODELAY set
* Connected to 10.212.128.82 (10.212.128.82) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.212.128.82:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 17 Oct 2021 06:29:55 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
Hello World!
* Connection #0 to host 10.212.128.82 left intact

問題なさそうですね。続いて、ノードを跨いで Pod 間で通信できるか見てみましょう。ingress-gceDefaultBackend の実装であり、常に 404 を返す l7-default-backend を使って確認してみます。

❯ kubectl get pods -owide -n kube-system -l k8s-app=glbc
NAME                                  READY   STATUS    RESTARTS   AGE   IP               NODE                                   NOMINATED NODE   READINESS GATES
l7-default-backend-56cb9644f6-wbmvq   1/1     Running   0          17h   10.212.128.200   gke-xxxxxxxxxxxxxxxxxx-ff19008d-gwdr   <none>           <none>

l7-default-backend から 404 が返ってきているのでこちらも問題なさそうです。

# ip netns exec helloworld curl -I http://10.212.128.200:8080
HTTP/1.1 404 Not Found
Date: Sun, 17 Oct 2021 05:23:05 GMT
Content-Length: 74
Content-Type: text/plain; charset=utf-8

hostNetwork が有効な場合の挙動は、各ノードで動作している手頃な flunetbit-gke を使って確認することができます。

❯ kubectl get pods -owide -n kube-system -l k8s-app=fluentbit-gke
NAME                  READY   STATUS    RESTARTS   AGE   IP            NODE                                   NOMINATED NODE   READINESS GATES
fluentbit-gke-ldsxh   2/2     Running   0          19h   10.212.0.32   gke-xxxxxxxxxxxxxxxxxx-ff19008d-gwdr   <none>           <none>
fluentbit-gke-vdhc6   2/2     Running   0          17h   10.212.0.41   gke-xxxxxxxxxxxxxxxxxx-cc5d1b58-2pvq   <none>           <none>

fluentbit-gke は 2020 番ポートで公開されたエンドポイントに対して livenessProbe が設定されているので、こちらにリクエストを投げてみます。

❯ kubectl get ds -n kube-system fluentbit-gke -o yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  (...)
  name: fluentbit-gke
  namespace: kube-system
spec:
    (...)
  template:
    (...)
    spec:
      containers:
      - image: gke.gcr.io/fluent-bit:v1.5.7-gke.1
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 2020
            scheme: HTTP
          (...)
        name: fluentbit
    (...)

まずは、まずは同一ノード上の hostNetwork が有効になっている fluentbit-gke にリクエストを投げてみます。200 OK が返ってきており、問題なく疎通できていそうですね。

# ip netns exec helloworld curl -I http://10.212.0.32:2020/
HTTP/1.1 200 OK
Server: Monkey/1.7.0
Date: Sun, 17 Oct 2021 05:23:17 GMT
Transfer-Encoding: chunked

次に別のノード上で動作している hostNetwork が有効になっている fluentbit-gke にリクエストを投げてみます。こちらも同様に 200 OK が返ってくることが分かると思います。

# ip netns exec helloworld curl -I http://10.212.0.41:2020/
HTTP/1.1 200 OK
Server: Monkey/1.7.0
Date: Sun, 17 Oct 2021 05:23:29 GMT
Transfer-Encoding: chunked

問題なさそうですね。最後に network namespace 内からループバックアドレスに対しての通信はどうでしょうか?こちらも helloworld-go 自身のプロセスに対してリクエストを投げて確認します。

# ip netns exec helloworld curl -I http://127.0.0.1:8080/

レスポンスが返って来ないですね。network namespace の中のループバックインターフェイスがどういう状態になっているか確認しましょう。

# ip netns exec helloworld ip addr list lo
1: lo: <LOOPBACK> mtu 65536 qdisc noqueue state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever

state DOWN となっています。containerd に読み込まれた CNI の設定を見返してみると cni-loopback というネットワーク設定があることが分かります。そちらを実行してみましょう。

# echo '{ "cniVersion": "0.3.1", "name": "cni-loopback", "type": "loopback" }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/loopback
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "lo",
            "mac": "00:00:00:00:00:00",
            "sandbox": "/var/run/netns/helloworld"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 0,
            "address": "127.0.0.1/8"
        },
        {
            "version": "6",
            "interface": 0,
            "address": "::1/128"
        }
    ],
    "dns": {}
}

これで、network namespace の中のループバックインターフェイスに変化があったはずです。確認してみましょう。

# ip netns exec helloworld ip addr list lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

state が UNKNOWN となっていますが、もう一度 curl を実行してみましょう。今度はレスポンスが返ってくるようになりました。ループバックインターフェイスが正しく動作していそうですね。

# ip netns exec helloworld curl http://127.0.0.1:8080
Hello World!

では、一旦、CNI plugin で作成した諸々を削除します。作成した時とは逆順で実行していきます。削除する際は prevResult のフィールドは不要です。

# Deleting network configuration for loopback
echo '{ "cniVersion": "0.3.1", "name": "cni-loopback", "type": "loopback" }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/loopback

# Deleting network configuration set by bandwidth plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "bandwidth", "capabilities": { "bandwidth": true } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/bandwidth

# Deleting network configuration set by portmap plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "portmap", "capabilities": { "portMappings": true } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/portmap

# Deleting network configuration set by gke plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "gke", "mtu": 1460, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "10.212.128.64/26"}] ], "routes": [ {"dst": "0.0.0.0/0"} ] } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/gke

PTP (Point-to-Point) CNI Plugin

これまで GKE CNI plugin でやってきた実験を PTP CNI plugin に置き換えて行なってみましょう。挙動はどう変わるでしょうか。

# Adding network configuration set by gke plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "ptp", "mtu": 1460, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "10.212.128.64/26"}] ], "routes": [ {"dst": "0.0.0.0/0"} ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/ptp

# Adding network configuration set by portmap plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "portmap", "capabilities": { "portMappings": true }, "prevResult": { "interfaces": [ { "name": "veth9c82d586", "mac": "e6:83:e4:98:5e:91" }, { "name": "eth0", "mac": "56:ae:79:83:c4:36", "sandbox": "/var/run/netns/helloworld" } ], "ips": [ { "version": "4", "interface": 1, "address": "10.212.128.83/26", "gateway": "10.212.128.65" } ], "routes": [ { "dst": "0.0.0.0/0" } ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/portmap

# Adding network configuration set by portmap plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "bandwidth", "capabilities": { "bandwidth": true }, "prevResult": { "interfaces": [ { "name": "veth9c82d586", "mac": "e6:83:e4:98:5e:91" }, { "name": "eth0", "mac": "56:ae:79:83:c4:36", "sandbox": "/var/run/netns/helloworld" } ], "ips": [ { "version": "4", "interface": 1, "address": "10.212.128.83/26", "gateway": "10.212.128.65" } ], "routes": [ { "dst": "0.0.0.0/0" } ] } }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/bandwidth

# Adding network configuration for loopback
echo '{ "cniVersion": "0.3.1", "name": "cni-loopback", "type": "loopback" }' | CNI_COMMAND=ADD CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/loopback

ホストマシン上の仮想ネットワークインターフェイスの名前が違うだけで出力は同じですね。今回は 10.212.128.83 の IP が払い出されたようです。

# # ホストマシン上に仮想ネットワークインターフェイスが作成されている
# ip addr list veth9c82d586
8: veth9c82d586@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue state UP group default
    link/ether e6:83:e4:98:5e:91 brd ff:ff:ff:ff:ff:ff link-netns helloworld
    inet 10.212.128.65/32 brd 10.212.128.65 scope global veth9c82d586
       valid_lft forever preferred_lft forever
    inet6 fe80::e483:e4ff:fe98:5e91/64 scope link
       valid_lft forever preferred_lft forever

# # ホストマシン上のルートテーブルも同じものが設定されている
# ip route
default via 10.212.0.1 dev ens4 proto dhcp src 10.212.0.41 metric 100
10.212.0.1 dev ens4 proto dhcp scope link src 10.212.0.41 metric 100
(...)
10.212.128.83 dev veth9c82d586 scope host
169.254.123.0/24 dev docker0 proto kernel scope link src 169.254.123.1 linkdown

# # network namespace に仮想ネットワークインターフェイスが繋がっており、払い出された IP も一致
# ip netns exec helloworld ip addr list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
9: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue state UP group default
    link/ether 56:ae:79:83:c4:36 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.212.128.83/26 brd 10.212.128.127 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::54ae:79ff:fe83:c436/64 scope link
       valid_lft forever preferred_lft forever

# # network namespace 内のルートテーブルも同じルール
# ip netns exec helloworld ip route
default via 10.212.128.65 dev eth0
10.212.128.64/26 via 10.212.128.65 dev eth0 src 10.212.128.83
10.212.128.65 dev eth0 scope link src 10.212.128.83

# # ホストマシンから helloworld コンテナの network namespace への疎通も可能
# ip netns exec helloworld curl http://10.212.128.83:8080/
Hello World!

# # 別ノードの Pod の network namespace への疎通も可能
# ip netns exec helloworld curl -I http://10.212.128.200:8080
HTTP/1.1 404 Not Found
Date: Sun, 17 Oct 2021 05:23:05 GMT
Content-Length: 74
Content-Type: text/plain; charset=utf-8

# # 同一ホスト上の Pod 間のパケットのやり取りも問題なし
# ip netns exec helloworld curl -I http://10.212.0.32:2020
HTTP/1.1 200 OK
Server: Monkey/1.7.0
Date: Sun, 17 Oct 2021 06:41:39 GMT
Transfer-Encoding: chunked

# # 異なるホスト上の Pod 間でのパケットのやり取りも問題なし
# ip netns exec helloworld curl -I http://10.212.0.41:2020
HTTP/1.1 200 OK
Server: Monkey/1.7.0
Date: Sat, 16 Oct 2021 07:07:50 GMT
Transfer-Encoding: chunked

ということで、ホスト上に作成された仮想ネットワークインターフェイスの命名規則 (prefix とランダム文字列の生成方法) 以外に大きな違いはないように見えます。

  • gke3a52ce78095
  • vetha20795a3

忘れないうちに仮想ネットワークインターフェイスを削除します。

# Deleting network configuration for loopback
echo '{ "cniVersion": "0.3.1", "name": "cni-loopback", "type": "loopback" }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/loopback

# Deleting network configuration set by bandwidth plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "bandwidth", "capabilities": { "bandwidth": true } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/bandwidth

# Deleting network configuration set by portmap plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "portmap", "capabilities": { "portMappings": true } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/portmap

# Deleting network configuration set by gke plugin
echo '{ "cniVersion": "0.3.1", "name": "gke-pod-network", "type": "gke", "mtu": 1460, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "10.212.128.83/26"}] ], "routes": [ {"dst": "0.0.0.0/0"} ] } }' | CNI_COMMAND=DEL CNI_CONTAINERID=helloworld CNI_NETNS=/var/run/netns/helloworld CNI_IFNAME=eth0 CNI_PATH=/home/kubernetes/bin /home/kubernetes/bin/gke

最後に作成した network namespace やコンテナをお片付けします。

# network namespace の削除
ip netns delete helloworld

# コンテナを停止
ctr -n k8s.io tasks kill helloworld

# コンテナの削除
ctr -n k8s.io containers rm helloworld

# イメージの削除
ctr -n k8s.io images rm gcr.io/knative-samples/helloworld-go:latest

ここまで整理すると、

  • CNI の設定ファイルを netd が作成
  • GKE のメインの CNI plugin は gke
  • GKE CNI plugin は公式のリファレンス実装である PTP CNI plugin とほぼ同じ実装 (?)

最後の点に関して、私は Google の中の人ではないので確証があるわけではありません。ただ、今回の実験結果と以下の考察から機能的には同じなのでわざわざ公開するまでもなかったのではと考えています。

  • GKE CNI plugin が独自実装であるなら AWS の amazon-vpc-cni-k8s や Azure の Azure VNET CNI Plugins のように公開されていてもおかしくありません。
  • 仮に独自実装だとすると netd は公開されているのに、GKE CNI plugin だけが公開されていないことに違和感を覚えます。
  • kubernetes/kubernetes (k/k) に大量に GKE 独自実装のコードが残っているので、CNI の実装だけクローズドにする理由がよく分からないです。
  • PTP CNI plugin の PR で Google の中の人が GCP で PTP plugin を使おうとしているとコメントしていました。そのために必要な修正を PR として投げていたようです。ただ、kube-proxy/IPVS: arp_ignore and arp_announce break some CNI plugins にあるように Kubernetes 1.13 の変更により CNI plugin のいくつかが動作しなくなり、この PR の修正内容も同様に動かなくなったためマージされることはありませんでした。GKE CNI plugin を使った場合でもホスト側の仮想ネットワークインターフェイスに GW の IP が付与されていたり、この PR で設定されているカーネルパラメータも設定されていないので内部的にも使われていないはずです。
  • Kubernetes の SIG-Network の chair で GKE 関連でも有名な Tim Hockin が aws-vpc-cni-k8s に対して Issue で これって PTP plugin と何が違うの?公式の IPAM plugin 使わないのはなぜ? と質問していました。(AWS の場合は、ENI にセキュリティグループが紐付いたり、ENI の払い出しや管理を L-IPAM でやりたいため独自実装となっているようです。) GKE が PTP と host-local IPAM plugin の組み合わせ設計を検討する中で他のクラウドプロバイダーの実装をチェックしていたとすると面白いですが、実際にどういう意図で質問したのかまでは読み取れませんでした。

Pause container

本題とは話が逸れるのですが、今回 gcr.io/knative-samples/helloworld-go:latest のイメージを使ってコンテナを動かして実験を行いました。しかし、kubelet は今回の実験のようにユーザーのコンテナに対して network namespace を分離するのではなく、pause コンテナを起動してその子プロセスとしてユーザーのコンテナを実行します。pause コンテナは kubelet の --pod-infra-container-image オプションにあるように Infra container と呼ばれたり、containerd の RunPodSandbox API にあるように Pod sandbox や Sandbox container などと呼ばれるものです。pause コンテナに関しては有名なブログ記事 The Almighty Pause Container があります。この記事や他の記事でも pause コンテナの存在意義として network namespace のセットアップと維持に必要と説明されることがあります。ただ、containerd ではホスト上の /var/run/netns に network namespace を作成しているため、pause コンテナがなくても network namespace を保持することは可能です。pause コンテナの存在意義は Zombie プロセスを掃除するための init プロセスとしての役割や Share Process Namespace between Containers in a Pod を既存の設計を変えずに提供できることにあるようです。詳細は Is the pause container necessary? の Issue の一連の議論にまとまっています。

PTP plugin internal

ここからは、GKE CNI plugin が PTP CNI plugin と同じ実装かどうか、ptp v0.8.5 ソースコードを読みながら実装を追っていきます。

今回は、cmdAdd の実装だけ追うことにします。前半は IPAM plugin を実行してその結果を取得しているだけなので飛ばします。

まず、ip.EnableForward() で同一ノード上で Pod 同士がパケットをやり取りできるようにしています。Google Compute Engine (GCE) の Cluster networking の説明でも少し触れられていることです。

// https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L221-L223
    if err := ip.EnableForward(result.IPs); err != nil {
        return fmt.Errorf("Could not enable IP forwarding: %v", err)
    }

IPAM plugin が払い出した Pod IP のリストを走査して IPv4/IPv6 かによって異なるカーネルパラメータを設定しています。IPv4 の場合は VPN サーバを構築したことがある人には馴染みがあるかもしれない ip_forward のパラメータを有効にしています。

// https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/ipforward_linux.go#L24-L26
func EnableIP4Forward() error {
    return echo1("/proc/sys/net/ipv4/ip_forward")
}

次に、GKE CNI plugin に引数として渡した network namespace に対して、setupContainerVeth() で仮想ネットワークインターフェイスをセットアップします。

// https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L231-L234
    hostInterface, containerInterface, err := setupContainerVeth(netns, args.IfName, conf.MTU, result)
    if err != nil {
        return err
    }

network namespace 側の仮想ネットワークインターフェイスのセットアップは少し hacky なことをしていて、GKE CNI plugin で実験をした時に 10.212.128.82/32 ではなく 10.212.128.82/26 の IP が紐付いていたことにも関連があります。

# ip netns exec helloworld ip addr list eth0
7: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue state UP group default
    link/ether 46:63:4b:88:30:5e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.212.128.82/26 brd 10.212.128.127 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4463:4bff:fe88:305e/64 scope link
       valid_lft forever preferred_lft forever

ソースコードのコメントに hacky な方法をとった背景と実装方法が説明されています。

// https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L53-L63
func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
    // The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1.
    // What we want is really a point-to-point link but veth does not support IFF_POINTTOPOINT.
    // Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
    // add a route like "192.168.3.0/24 via 192.168.3.1 dev $ifName".
    // Unfortunately that won't work as the GW will be outside the interface's subnet.

    // Our solution is to configure the interface with 192.168.3.5/24, then delete the
    // "192.168.3.0/24 dev $ifName" route that was automatically added. Then we add
    // "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName".
    // In other words we force all traffic to ARP via the gateway except for GW itself.

実装を見てどういう ip コマンドを発行しているのか確認してみましょう。

その前に、network namespace の仮想ネットワークインターフェイスをセットアップする処理が netns.Do() を使っていることへの補足です。これは、Namespaces, Threads, and Go に説明があるように Linux と Go におけるスレッドモデルに違いがあり、Go の場合 goroutine を切り替えると network namespace も切り替わってしまう可能性があるためです。

// https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L68
    err := netns.Do(func(hostNS ns.NetNS) error {
        (...)
    })

では、処理を追っていきます。ip.SetupVeth()ifName, mtu, hostNS をそれぞれ渡してホストマシンとコンテナの network namespace に取り付ける仮想ネットワークインターフェイスのペアを作っています。今回の実験で言うと ifNameeth0, mtu が 1460, hostNShelloworld です。

// https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L69-L72
        hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
        if err != nil {
            return err
        }

ip.SetupVeth() は内部的に ip.SetupVethWithName() を呼び出しています。その時に、PTP plugin の場合は、ip.SetupVethWithName()hostVethName に空白を渡すことでランダム文字列として生成された仮想ネットワークインターフェイスの名前をホストマシンで使うようになっています。

// https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L173-L175
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
    return SetupVethWithName(contVethName, "", mtu, hostNS)
}

ランダム文字列を生成している箇所です。PTP CNI plugin と GKE CNI plugin でホスト側に生成される仮想ネットワークインターフェイスの名前が違っていたのは、この実装が違うからだと思われます。また、GKE の場合は、コンテナ ID が同じ場合に仮想ネットワークインターフェイスの名前が同じになる挙動をしていました。ちなみに、Google の人が Make host-side veth name configurable. #344 で CNI plugin で提供しているライブラリに関数を追加しているので、この関数を使ってホスト側の仮想ネットワークインターフェイスの名前を変更しているのだと思われます。

// https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L97-L107
// RandomVethName returns string "veth" with random prefix (hashed from entropy)
func RandomVethName() (string, error) {
    entropy := make([]byte, 4)
    _, err := rand.Reader.Read(entropy)
    if err != nil {
        return "", fmt.Errorf("failed to generate random veth name: %v", err)
    }

    // NetworkManager (recent versions) will ignore veth devices that start with "veth"
    return fmt.Sprintf("veth%x", entropy), nil
}

記事が長くなり過ぎたので、ここからは ip コマンドと対応するソースコードの場所をマッピングするだけにします。PTP plugin で実験した時の値で対応するコマンドを書き並べます。

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L34-L45
ip netns exec helloworld ip link add eth0 mtu 1460 type veth peer name vetha20795a3 mtu 1460

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L139-L141
ip netns exec helloworld ip link set eth0 up

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L148-L150
ip netns exec helloworld ip link set vetha20795a3 netns 1

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ip/link_linux.go#L152-L162
ip link set vetha20795a3 up

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ipam/ipam_linux.go#L85-L88
ip netns exec helloworld ip addr add 10.212.128.83/26 dev eth0

# https://github.com/containernetworking/plugins/blob/v0.8.5/pkg/ipam/ipam_linux.go#L112-L117
ip netns exec helloworld ip route add 0.0.0.0/0 via 10.212.128.65 scope global dev eth0

# https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L96-L108
ip netns exec helloworld ip route del 10.212.128.64/26 dev eth0

# https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L115-L139
ip netns exec helloworld ip route add 10.212.128.65 dev eth0 scope link src 10.212.128.83
ip netns exec helloworld ip route add 10.212.128.64/26 via 10.212.128.65 dev eth0 src 10.212.128.83

# https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L170-L177
ip addr add 10.212.128.65/32 brd 10.212.128.65 dev vetha20795a3

# https://github.com/containernetworking/plugins/blob/v0.8.5/plugins/main/ptp/ptp.go#L179-L186
ip route add 10.212.128.83 dev vetha20795a3 scope host

GKE CNI plugin で作成された仮想ネットワークインターフェイスとルーティングテーブルが PTP CNI plugin と一致するため、やはりコア部分の実装は同じように見えます。

Wrapped up

本記事では、GKE CNI plugin の実装を「実験」と「PTP CNI plugin との比較」から深掘りしました。どういう実装になっているか確証が持てないためモヤモヤする部分もありますが、どちらの Main CNI plugin も共通点が多くあることが分かりました。GKE のネットワーク設計が更に大きく変化する Dataplane V2 への移行検証を今後進めていく予定ですので、そちらの実装の詳細が見えてきたらまた記事にまとめようと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
14