Google Cloud の GKE (Google Kubernetes Engine) では、Calico と Dataplane V2 という 2 つのネットワーク制御の仕組みが提供されています。
どちらも Kubernetes NetworkPolicy を利用できるようになっていますが、Calico は iptables を利用してルーティングを制御する一方、Dataplane V2 は Cilium をベースとして eBPF によってルーティングを制御します。
今回は、Calico から GKE Dataplane V2 への移行を考えるにあたって、NetworkPolicy 関連で検証したことの一部をご紹介します。
GKE Dataplane V2 とは
GKE Dataplane V2 (以降、Dataplane V2) は Cilium をベースとした GKE のネットワーク制御の仕組みで、2020年8月に発表されました。※1
kube-proxy や iptables に依存しなくなることでネットワーク制御のパフォーマンス向上が期待できるほか、ネットワークポリシーロギング の機能によって NetworkPolicy の適用結果 (Allow / Deny) をログに出力する機能を備えるなど、従来の Calico CNI に基づくネットワーク制御では得られなかった複数のメリットがあります。
現時点では、Calico を用いた既存のネットワーク制御の仕組みを完全に置き換えるものではない(並行して提供されている)ため、Standard クラスタであればユーザがどちらかを選択して利用することになります(Autopilot では Dataplane V2 が採用されており、これを変更することはできません)。
なお、現時点では既存の GKE Standard クラスタを Calico から Dataplane V2 に移行する手段は提供されていないため、移行する場合は新規にクラスタを作成する必要があります。
Dataplane V2 は Cilium をベースとしているものの、あくまで Google 独自の実装であり、Cilium と完全な互換性があるものではありません。
例えば、Cilium では Kubernetes NetworkPolicy のほかに、拡張された NetworkPolicy である CiliumNetworkPolicy や CiliumClusterwideNetworkPolicy を併用することができますが、現在の GKE (1.21.5-gke.1300 以降) ではこれらの Cilium CRD はサポートされていません。※2
機能によっては Dataplane V2 側に代替実装が用意されている場合もあります。
例えば、L7 の FQDN に基づく NetworkPolicy を定義したい場合、Cilium であれば CiliumNetworkPolicy がその役割を担っていますが、Dataplane V2 の場合は FQDNNetworkPolicy が同様の目的を果たす手段として用意されています(FQDNNetworkPolicy については以前検証したブログ記事がありますので、ご興味があれば こちら も併せてどうぞ)。
なお、両者は YAML の書き方が異なり、機能にも差分があるため、同じものとして扱うことはできません。
※1 余談も余談ですが、Google Cloud には GKE Dataplane V2 と名前の似た Cloud Interconnect Dataplane V2 というものがありますが、これらは単純に第二世代という意味であり、実装に強い関連性を持っているわけではありません。例えば、Cloud Interconnect Dataplane V2 は Cilium や eBPF の技術とは無関係(のはず)です。
※2 "GKE バージョン 1.21.5-gke.1300 以降、GKE Dataplane V2 は CiliumNetworkPolicy または CiliumClusterwideNetworkPolicy CRD API をサポートしていません。" (Dataplane V2 の 制限事項 より)
今回の検証ポイントと背景
Calico から Dataplane V2 に安全に移行するためには、様々な観点で検証が必要になると考えられます。
今回は、Calico と Dataplane V2 の Kubernetes NetworkPolicy に関する機能差分に着目し、Dataplane V2 では無効となる構成を実際に Apply してみた場合の挙動を調べてみました。
GKE のこちらの 公式ドキュメント によると、Dataplane V2 では、NetworkPolicy を定義する際に以下のような制約があることがわかります。
これらの制約に該当する構成は、「無効」になると記載されています。
- ipBlock.cidr フィールドに Pod または Service の IP アドレスを使用することはできない(代わりに、ラベルを指定して対象のワークロードを特定する必要がある)
- 空の ports.port フィールドを指定することはできない(ポートを指定する際は必ずプロトコルも指定する必要がある)
Calico からの移行という前提に立つと、大量に存在する NetworkPolicy の中に上記制約に該当するものが混在しており、NetworkPolicy マニフェストの単純な移行を試みて無効な構成を Apply しようとしてしまう、といった可能性は考えられそうです。
上記制約の個々は決して深刻なものではありませんが、これが全てとは限らず、今後 Calico の発展や Dataplane V2 としての実装の都合上などにより他にも Calico との違いが生まれてくる可能性が考えられます。
そうした場合に、構成が「無効」になる、とは具体的にはどういった挙動を示すのでしょうか。
「無効」な構成の NetworkPolicy マニフェストを Apply すると Dataplane V2 はどうなるのか、そもそも Apply できるのか、といった挙動を確認してみようというのが今回の主旨です。
※ ちなみに、上記の制約は Cilium の 公式ドキュメント でも Cilium の Kubernetes NetworkPolicy における Known Issue として紹介されているため、Dataplane V2 独自の制約というわけではなさそうです
実機検証
それでは実際に GKE Standard のクラスタを用意して検証してみましょう。
今回は以下のとおり、Dataplane V2 を有効化した GKE 1.27.3-gke.100 のクラスタを用意しました。
$ gcloud container clusters describe sample-cluster addonsConfig: dnsCacheConfig: enabled: true networkPolicyConfig: disabled: true ... autopilot: {} clusterIpv4Cidr: 10.32.0.0/14 currentMasterVersion: 1.27.3-gke.100 currentNodeCount: 3 currentNodeVersion: 1.27.3-gke.100 ipAllocationPolicy: clusterIpv4Cidr: 10.32.0.0/14 clusterIpv4CidrBlock: 10.32.0.0/14 clusterSecondaryRangeName: gke-sample-cluster-pods-099d0671 servicesIpv4Cidr: 10.36.0.0/20 servicesIpv4CidrBlock: 10.36.0.0/20 servicesSecondaryRangeName: gke-sample-cluster-services-099d0671 stackType: IPV4 useIpAliases: true ... location: us-central1 name: sample-cluster network: default networkConfig: datapathProvider: ADVANCED_DATAPATH defaultSnatStatus: {} network: projects/****************/global/networks/default serviceExternalIpsConfig: {} subnetwork: projects/****************/regions/us-central1/subnetworks/default ... nodeConfig: machineType: e2-medium ...
Dataplane V2 という表記はありませんが、networkConfig.datapathProvider が ADVANCED_DATAPATH
になっていることから、このクラスタで Dataplane V2 が有効化されていることがわかります(Calico CNI を利用する場合は networkConfig.datapathProvider が LEGACY_DATAPATH
, networkPolicy.provider が CALICO
になります)。
また、Dataplane V2 の場合は networkPolicy.provider のフィールドがそもそも存在しないようですが、これは Dataplane V2 の場合デフォルトで NetworkPolicy が有効化されている(Disable できない)ためと考えられます。
次に、NetworkPolicy の動作確認のため、適当な Pod を 3 つほどクラスタに展開します。
$ kubectl run pod1 --image nginx pod/pod1 created $ kubectl run pod2 --image nginx pod/pod2 created $ kubectl run pod3 --image nginx pod/pod3 created $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod1 1/1 Running 0 16s 10.32.2.8 gke-sample-cluster-default-pool-c40d772e-l3kh <none> <none> pod2 1/1 Running 0 9s 10.32.2.9 gke-sample-cluster-default-pool-c40d772e-l3kh <none> <none> pod3 1/1 Running 0 5s 10.32.2.10 gke-sample-cluster-default-pool-c40d772e-l3kh <none> <none>
検証用の Kubernetes NetworkPolicy のマニフェストは以下を用意しました。
Dataplane V2 では「無効」な構成とされている、ipBlock.cidr フィールドに Pod の IP アドレスを使用したポリシとなっています。
今回は動作検証が目的であるためあえてこのようなポリシを作成していますが、一般的にはクラスタ内の Pod に対して ipBlock でポリシを作成するのはアンチパターンです。
Pod の IP アドレスは動的に付与され、稼働中に再スケジュールなどで変動する可能性があります。
$ cat testnetpol.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: test-network-policy namespace: default spec: podSelector: {} policyTypes: - Ingress ingress: - from: - ipBlock: cidr: 10.32.2.8/32 egress: - {}
この無効なマニフェストをクラスタに Apply してみるとどうなるでしょうか。
$ kubectl apply -f testnetpol.yaml apiVersion: networking.k8s.io/v1 networkpolicy.networking.k8s.io/test-network-policy created $ kubectl describe networkpolicy test-network-policy Name: test-network-policy Namespace: default Created on: 2023-09-23 14:20:57 +0000 UTC Labels: <none> Annotations: <none> Spec: PodSelector: <none> (Allowing the specific traffic to all pods in this namespace) Allowing ingress traffic: To Port: <any> (traffic allowed to all ports) From: IPBlock: CIDR: 10.32.2.8/32 Except: Not affecting egress traffic Policy Types: Ingress
Apply することができました。
無効といえど、Apply の時点でバリデーションが行われるということではなさそうです。
次に、この NetworkPolicy がどのように動作するのかを見てみましょう。
今回のポリシは、Ingress を pod1 の IP アドレス(10.32.2.8/32)からのみ許可し、Egress に関しては関与しない(すべて許可)、という設定になっています。
従って、pod1 から他の Pod への通信は(このポリシが正常に機能すれば)許可されるはずです。
$ kubectl exec -it pod1 -- sh # curl 10.32.2.10 (タイムアウト) # exit command terminated with exit code 130 $ kubectl delete -f testnetpol.yaml networkpolicy.networking.k8s.io "test-network-policy" deleted $ kubectl exec -it pod1 -- sh # curl 10.32.2.10 ...(略)... Welcome to nginx! ...(略)...
結果、pod1 から pod3 (10.32.2.10) への curl は失敗となりました。
該当の NetworkPolicy を取り除くと、当然ながら pod1 から他の Pod へのリクエストは正常に到達可能になりました。
Dataplane V2 の場合は、ネットワークポリシーロギングを利用できるため、ログエントリからも動作の様子を確認してみましょう。
このクラスタの NetworkLogging の設定は以下のとおりです。
予め Allow / Deny のどちらの場合でも NetworkPolicy の動作に関するログが Cloud Logging に出力されるよう設定しています。
$ kubectl describe networklogging Name: default Namespace: Labels: addonmanager.kubernetes.io/mode=EnsureExists Annotations: components.gke.io/component-name: advanceddatapath components.gke.io/component-version: 27.6.0 components.gke.io/layer: addon API Version: networking.gke.io/v1alpha1 Kind: NetworkLogging Metadata: Creation Timestamp: 2023-09-22T15:19:29Z Generation: 2 Resource Version: 778820 UID: 7df685fc-397d-4f43-9d81-ff88d6ad7863 Spec: Cluster: Allow: Delegate: false Log: true Deny: Delegate: false Log: true Events: <none>
出力されたログエントリを Cloud Logging から確認してみます。
上記の curl コマンドに対して、6 件のログエントリが生成されていました。
今回の場合は、定義したポリシが pod1 と pod3 の両方を対象とするため、pod1 から pod3 への curl リクエストは pod1 からの Egress と pod3 への Ingress の 2 つのタイミングで評価されていることがわかります。
1 件目のエントリでは、pod1 からの Egress トラフィックが許可されていることがわかります。
{ "insertId": "dhickie3cds9bxp7", "jsonPayload": { "dest": { "pod_namespace": "default", "namespace": "default", "pod_name": "pod3" }, "disposition": "allow", "node_name": "gke-sample-cluster-default-pool-c40d772e-l3kh", "count": 1, "src": { "namespace": "default", "pod_namespace": "default", "pod_name": "pod1" }, "connection": { "dest_ip": "10.32.2.10", "protocol": "tcp", "src_port": 48512, "dest_port": 80, "direction": "egress", "src_ip": "10.32.2.8" }, "policies": [ { "kind": "NetworkPolicy", "name": "test-network-policy", "namespace": "default" } ] }, "resource": { "type": "k8s_node", "labels": { "location": "us-central1", "project_id": "****************", "cluster_name": "sample-cluster", "node_name": "gke-sample-cluster-default-pool-c40d772e-l3kh" } }, "timestamp": "2023-09-23T14:21:51.384375643Z", "logName": "projects/****************/logs/policy-action", "receiveTimestamp": "2023-09-23T14:21:54.247088734Z" }
2 件目のエントリでは、pod3 への Ingress トラフィックが拒否されていることが確認できます。
3 件目以降のエントリは curl の再試行によるもので、2 件目とログの内容は同じです。
{ "insertId": "akf4ygd85sv6au09", "jsonPayload": { "disposition": "deny", "node_name": "gke-sample-cluster-default-pool-c40d772e-l3kh", "count": 3, "src": { "pod_name": "pod1", "pod_namespace": "default", "namespace": "default" }, "dest": { "pod_name": "pod3", "namespace": "default", "pod_namespace": "default" }, "connection": { "direction": "ingress", "protocol": "tcp", "dest_ip": "10.32.2.10", "src_port": 48512, "src_ip": "10.32.2.8", "dest_port": 80 } }, "resource": { "type": "k8s_node", "labels": { "location": "us-central1", "project_id": "****************", "node_name": "gke-sample-cluster-default-pool-c40d772e-l3kh", "cluster_name": "sample-cluster" } }, "timestamp": "2023-09-23T14:21:51.384394882Z", "logName": "projects/****************/logs/policy-action", "receiveTimestamp": "2023-09-23T14:21:59.247442915Z" }
1 件目と 2 件目のエントリを見比べると、1 件目は評価した NetworkPolicy のリソース名 (test-network-policy) がログエントリに含まれているのに対し、2 件目には含まれていませんでした。
考察・検証から得られた気付き
Dataplane V2 における無効な NetworkPolicy がどのような影響を与えるのか、上記の検証を通じて得られたいくつかの気付きを共有します。
まず、無効なマニフェストであってもクラスタに Apply することは可能なようです。
無効なポリシの対象となったトラフィックはタイムアウトし、明示的なエラーは返却されません。
加えて、Calico CNI を利用したクラスタで利用している NetworkPolicy を単純移行しようとすると無効なポリシも含めて Apply には成功してしまいます。
そのため、場合によっては無効なポリシが悪影響を及ぼしていることに気付くのが遅れるといった可能性がありそうです。
今回は折角 Dataplane V2 を利用しているため、ネットワークポリシーロギングからも無効なポリシに関する挙動を確認しました。
ログエントリの内容にそれを判別できる特徴があるのであれば、NetworkPolicy が期待した動作をしない場合に無効な構成を Apply してしまっている可能性に気付けるきっかけにできます。
検証では、pod1 からの Egress の許可には評価した NetworkPolicy のリソース名が含まれ、pod3 への Ingress の拒否にはリソース名が含まれていなかったことから、ネットワークポリシが無効な設定の場合はログエントリにリソース名が表示されないのでは(無効なポリシなので、そもそも Ingress を正しく評価できる NetworkPolicy がいない → policies フィールドが空になる)と想定しました。
しかし、以下のように有効な構成と思われる AllDeny のポリシを Apply した場合であっても、Deny ログには policies フィールドが含まれていない(拒否の理由となった NetworkPolicy のリソース名はログエントリからわからない)ため、どうやら単純に拒否 (Deny) のログエントリは「拒否の理由が、正しく評価できる NetworkPolicy であるか、無効なポリシが Apply されているためかに関わらず、policies フィールドが含まれない」ようです。
そのため、ネットワークポリシーロギングのエントリから直接的に無効なポリシの存在に気付くのは難しそうですね。
$ cat alldeny.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all spec: podSelector: {} policyTypes: - Ingress - Egress $ kubectl apply -f alldeny.yaml networkpolicy.networking.k8s.io/default-deny-all created $ kubectl get netpol NAME POD-SELECTOR AGE default-deny-all <none> 44m $ kubectl exec pod1 -it -- sh # curl 10.32.2.10 (タイムアウト) # exit command terminated with exit code 130 $ gcloud logging read 'insertId=o0qc79kobivrxvmm' --- insertId: o0qc79kobivrxvmm jsonPayload: connection: dest_ip: 10.32.2.10 dest_port: 80 direction: egress protocol: tcp src_ip: 10.32.2.8 src_port: 42996 count: 3 dest: namespace: default pod_name: pod3 pod_namespace: default disposition: deny node_name: gke-sample-cluster-default-pool-c40d772e-l3kh src: namespace: default pod_name: pod1 pod_namespace: default logName: projects/****************/logs/policy-action receiveTimestamp: '2023-09-25T02:23:59.248034900Z' resource: labels: cluster_name: sample-cluster location: us-central1 node_name: gke-sample-cluster-default-pool-c40d772e-l3kh project_id: **************** type: k8s_node timestamp: '2023-09-25T02:23:50.903492410Z'
以上より、Calico から Dataplane V2 への移行というシナリオにおいて、既存の NetworkPolicy をひとまず投入してから挙動やログに基づいて構成を修正してゆく、というアプローチはちょっと難しそうです。
移行にあたって無効なポリシを Apply してしまうリスクに対しては、Dataplane V2 や Cilium の Known Issue ・制限事項などに該当するマニフェストがないかなど、事前の精査にウェイトを置くほうが良さそうですね。
まとめ
Calico CNI から GKE Dataplane V2 への移行というシチュエーションをもとに、Dataplane V2 において無効な NetworkPolicy がどのような影響を与えるのかについて、実機検証を交えて調べました。
パフォーマンスや可観測性など、既存ワークロードの移行においては色々と検討すべき事項があるかと思いますが、そのひとつの観点として記事の情報が何かのお役に立てば幸いです。