Kubernetes IPv6 デュアルスタック構成の解説

執筆者 : 山下雅喜


1. はじめに

前回の記事では、Docker コンテナで IPv6 を利用する方法を整理しました。

valinux.hatenablog.com

これに引き続き、本記事では、Kubernetes クラスタを IPv4-IPv6 デュアルスタック構成で構築する方法や、IPv6 通信がどのように行われているかを解説します。


2. Kubernetes や CNI の IPv6 サポート

Kubernetes

Kubernetes では、v1.9 で IPv6 シングルスタック構成の利用が、v1.16 で IPv4-IPv6 デュアルスタック構成の利用がサポートされました。
IPv6 オンリーのネットワークでシステムを利用しているケースは少ないでしょうから、デュアルスタック構成で IPv6 を利用するのが現実的な選択肢でしょう。

Kubernetes は複数のコンポーネントが互いに通信し合うことでクラスタの基盤部分が構成されていますが、今回はクラスタ上にデプロイするアプリケーション部分 (Pod, Service など) の通信を IPv6 化することを目指します。

Calico

Pod で IPv6 を利用するには、CNI Plugin も IPv6 をサポートしている必要があります。
IPv4-IPv6 デュアルスタック構成で利用できる CNI Plugin の中で、人気のある Calico を今回は利用します。
(※本記事執筆時点で、Weave Net や Flannel は IPv6 の利用をサポートしていません。)

Calico では、IPIP トンネルや VXLAN オーバーレイネットワークを用いてマルチノードの Pod 間の通信を行う仕組みがあり、標準設定では IPIP トンネルを利用するようになっています。
ただし、Calico は IPv6 の通信で IPIP や VXLAN の利用をがサポートしていないため、ノード同士が Pod 間のパケットをルーティングして通信を行う方式を取ることになります。
https://docs.projectcalico.org/archive/v3.15/networking/vxlan-ipip

なお、Calico の仕組みについては過去に以下の記事でも解説しています。

www.valinux.co.jp


3. デュアルスタック構成のクラスタ構築

前提構成

利用したソフトウェアおよびバージョンは次の通りです。

  • OS : Ubuntu 18.04.4 LTS
  • Linux Kernel : 4.15.0-111-generic
  • Docker : 19.03.12
  • Kubernetes : 1.18.5
  • Calico : 3.15.1

また、サーバは2台用意し、Master ノードと Worker ノードをそれぞれ1台ずつとしました。

ノードのネットワークは次のように IPv4-IPv6 デュアルスタック構成とし、ルーターをデフォルトゲートウェイとしています。

  • ノードネットワーク CIDR
    • IPv4 : 192.168.1.0/24
    • IPv6 : 2001:db8::/64

f:id:ymstmsys:20200715151524p:plain

Pod や Service の CIDR は、ノードのネットワークと重複しないよう次のようにします。

IPv4 はプライベートアドレスを利用することが一般的であるため、IPv6 も同様にユニークローカルユニキャストアドレスを利用するようにしています。
なお、本記事を参考に環境を構築する際は、RFC 4086 に従ってランダムに生成したユニークローカルユニキャストアドレスを利用するようにしてください。

Kubernetes クラスタの構築

次の公式ドキュメントにならってクラスタを構築し、IPv6 を利用するために必要な部分についてのみ記載します。

まず、ノードが IPv6 パケットを転送できるようにするため、全ノードで次のようにカーネルパラメータを設定します。

$ sudo sysctl -w net.ipv6.conf.all.forwarding=1

次に、kubeadm を用いて Master ノードをセットアップします。

$ sudo kubeadm init \
    --feature-gates="IPv6DualStack=true" \
    --pod-network-cidr="10.244.0.0/16,fd00:1::/48" \
    --service-cidr="10.96.0.0/12,fd00::/108"

Kubernetes v1.18 では IPv6 デュアルスタック機能は標準では無効となっているため、パラメータで有効化します。
Pod CIDR および Service CIDR のパラメータには、IPv4 および IPv6 の CIDR をカンマ区切りで繋いで指定します。

今回は Kubernetes クラスタの基盤部分の IPv6 利用を行わないため、--apiserver-advertise-address パラメータで IPv6 アドレスの指定は行っていません。

その後、kubeadm を用いて Worker ノードをセットアップします。

$ sudo kubeadm join 192.168.1.11:6443 \
    --token XXXXXXXXXX \
    --discovery-token-ca-cert-hash sha256:XXXXXXXXXX

kubeadm init と join が成功すると、ノードの状態は次のようになります。
ノードの IPv4 アドレスが認識されています。

$ kubectl get node -o wide
NAME     STATUS     ROLES    AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION       CONTAINER-RUNTIME
master   NotReady   master   77s   v1.18.5   192.168.1.11   <none>        Ubuntu 18.04.4 LTS   4.15.0-111-generic   docker://19.3.12
worker   NotReady   <none>   44s   v1.18.5   192.168.1.12   <none>        Ubuntu 18.04.4 LTS   4.15.0-111-generic   docker://19.3.12

Calico のインストール

次の公式ドキュメントにならい、Calico をインストールします。

$ curl https://docs.projectcalico.org/archive/v3.15/manifests/calico.yaml -O

$ cp -p calico.yaml calico.yaml.org
$ vim calico.yaml   # 下記のように変更する

$ kubectl apply -f calico.yaml

calico.yaml は次のように変更しました。

$ diff -u calico.yaml.org calico.yaml
--- calico.yaml.org     2020-07-14 06:03:42.481570654 +0000
+++ calico.yaml 2020-07-14 06:08:11.183994061 +0000
@@ -16,7 +16,7 @@
   # - Otherwise, if VXLAN or BPF mode is enabled, set to your network MTU - 50
   # - Otherwise, if IPIP is enabled, set to your network MTU - 20
   # - Otherwise, if not using any encapsulation, set to your network MTU.
-  veth_mtu: "1440"
+  veth_mtu: "1500"
   # The CNI network configuration to install on each node. The special
   # values in this config will be automatically populated.
@@ -32,7 +32,9 @@
           "nodename": "__KUBERNETES_NODE_NAME__",
           "mtu": __CNI_MTU__,
           "ipam": {
-              "type": "calico-ipam"
+              "type": "calico-ipam",
+              "assign_ipv4": "true",
+              "assign_ipv6": "true"
           },
           "policy": {
               "type": "k8s"
@@ -3551,7 +3553,7 @@
               value: "autodetect"
             # Enable IPIP
             - name: CALICO_IPV4POOL_IPIP
-              value: "Always"
+              value: "Never"
             # Enable or Disable VXLAN on the default IP pool.
             - name: CALICO_IPV4POOL_VXLAN
               value: "Never"
@@ -3586,7 +3588,11 @@
               value: "ACCEPT"
             # Disable IPv6 on Kubernetes.
             - name: FELIX_IPV6SUPPORT
-              value: "false"
+              value: "true"
+            - name: IP6
+              value: "autodetect"
+            - name: CALICO_IPV6POOL_NAT_OUTGOING
+              value: "true"
             # Set Felix logging to "info"
             - name: FELIX_LOGSEVERITYSCREEN
               value: "info"

IPIP、VXLAN、Wireguard (Calico v3.15 から導入された Pod 間通信の暗号化機能) を利用しないため、MTU はノードの NIC と同じ値にしました。
CNI の ipam 設定では、Pod に IPv4 と IPv6 の両方のアドレスを割り当てるようにしています。

calico-node の環境変数は次のように変更しました。

環境変数名 デフォルト値 変更後の値 備考
CALICO_IPV4POOL_IPIP Always Never マルチノードの Pod 間の IPv4 通信で IPIP トンネルの利用を無効化する (IPv6 通信と合わせるため)
FELIX_IPV6SUPPORT false true IPv6 のサポートを有効にする
IP6 なし autodetect ノードの IPv6 アドレスの取得方法を自動検出モードにする
CALICO_IPV6POOL_NAT_OUTGOING なし true Pod からノードの外に IPv6 通信する際のIPマスカレードを有効化する

Calico のインストールが成功して正常に動作すると、Pod の状態は次のようになります。

$ kubectl get pod -A -o wide
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE     IP              NODE     NOMINATED NODE   READINESS GATES
kube-system   calico-kube-controllers-578894d4cd-lhcxq   1/1     Running   0          30s     10.244.219.64   master   <none>           <none>
kube-system   calico-node-rsl5t                          1/1     Running   0          30s     192.168.1.11    master   <none>           <none>
kube-system   calico-node-zphpn                          1/1     Running   0          30s     192.168.1.12    worker   <none>           <none>
kube-system   coredns-66bff467f8-2vrh4                   1/1     Running   0          2m36s   10.244.219.65   master   <none>           <none>
kube-system   coredns-66bff467f8-55mqb                   1/1     Running   0          2m36s   10.244.171.64   worker   <none>           <none>
kube-system   etcd-master                                1/1     Running   0          2m44s   192.168.1.11    master   <none>           <none>
kube-system   kube-apiserver-master                      1/1     Running   0          2m44s   192.168.1.11    master   <none>           <none>
kube-system   kube-controller-manager-master             1/1     Running   0          2m44s   192.168.1.11    master   <none>           <none>
kube-system   kube-proxy-5mbb5                           1/1     Running   0          2m21s   192.168.1.12    worker   <none>           <none>
kube-system   kube-proxy-pmkct                           1/1     Running   0          2m36s   192.168.1.11    master   <none>           <none>
kube-system   kube-scheduler-master                      1/1     Running   0          2m44s   192.168.1.11    master   <none>           <none>

これで、デュアルスタック構成のクラスタ構築が完了し、任意に Pod や Service を作成し、IPv4 と IPv6 のどちらでも利用できるようになりました。


4. IPv6 ネットワークの確認

クラスタ構築直後の状態で IPv6 ネットワークの設定や動作を確認していきます。

Master ノードのネットワーク設定

$ ip a
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
2: ens32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:2b:fd:72 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.11/24 brd 192.168.1.255 scope global ens32
       valid_lft forever preferred_lft forever
    inet6 2001:db8::11/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe2b:fd72/64 scope link
       valid_lft forever preferred_lft forever
5: calib8c5667857f@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
6: cali99739419c54@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever

$ ip -6 route
2001:db8::/64 dev ens32 proto kernel metric 256 pref medium
fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780/122 via 2001:db8::12 dev ens32 proto bird metric 1024 pref medium
fd00:1:0:db4d:f2f2:402d:3b99:a180 dev calib8c5667857f metric 1024 pref medium
fd00:1:0:db4d:f2f2:402d:3b99:a181 dev cali99739419c54 metric 1024 pref medium
blackhole fd00:1:0:db4d:f2f2:402d:3b99:a180/122 dev lo proto bird metric 1024 error -22 pref medium
fe80::/64 dev ens32 proto kernel metric 256 pref medium
fe80::/64 dev calib8c5667857f proto kernel metric 256 pref medium
fe80::/64 dev cali99739419c54 proto kernel metric 256 pref medium
default via 2001:db8::1 dev ens32 proto static metric 1024 pref medium

Worker ノードのネットワーク設定

$ ip a
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
2: ens32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:47:ea:f2 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.12/24 brd 192.168.1.255 scope global ens32
       valid_lft forever preferred_lft forever
    inet6 2001:db8::12/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe47:eaf2/64 scope link
       valid_lft forever preferred_lft forever
5: cali33c691eb8a3@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever

$ ip -6 route
2001:db8::/64 dev ens32 proto kernel metric 256 pref medium
fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 dev cali33c691eb8a3 metric 1024 pref medium
blackhole fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780/122 dev lo proto bird metric 1024 error -22 pref medium
fd00:1:0:db4d:f2f2:402d:3b99:a180/122 via 2001:db8::11 dev ens32 proto bird metric 1024 pref medium
fe80::/64 dev ens32 proto kernel metric 256 pref medium
fe80::/64 dev cali33c691eb8a3 proto kernel metric 256 pref medium
default via 2001:db8::1 dev ens32 proto static metric 1024 pref medium

Calico の IPAM ブロック

$ kubectl get ipamblocks -o custom-columns=NAME:.metadata.name,AFFINITY:.spec.affinity,CIDR:.spec.cidr
NAME                                    AFFINITY      CIDR
10-244-171-64-26                        host:worker   10.244.171.64/26
10-244-219-64-26                        host:master   10.244.219.64/26
fd00-1-0-ab7e-b4b6-dc9b-3ca1-a780-122   host:worker   fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780/122
fd00-1-0-db4d-f2f2-402d-3b99-a180-122   host:master   fd00:1:0:db4d:f2f2:402d:3b99:a180/122

Pod のネットワーク設定

Master ノード上の coredns Pod

$ sudo nsenter -t $(pidof coredns) -n ip a
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
3: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether f6:10:0b:04:6f:52 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.219.65/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fd00:1:0:db4d:f2f2:402d:3b99:a181/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::f410:bff:fe04:6f52/64 scope link
       valid_lft forever preferred_lft forever

$ sudo nsenter -t $(pidof coredns) -n ip -6 route
fd00:1:0:db4d:f2f2:402d:3b99:a181 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::ecee:eeff:feee:eeee dev eth0 metric 1024 pref medium

Worker ノード上の coredns Pod

$ sudo nsenter -t $(pidof coredns) -n ip a
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
3: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether d6:47:0b:3d:90:9c brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.171.64/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::d447:bff:fe3d:909c/64 scope link
       valid_lft forever preferred_lft forever

$ sudo nsenter -t $(pidof coredns) -n ip -6 route
fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::ecee:eeff:feee:eeee dev eth0 metric 1024 pref medium

Master ノード上の calico-kube-controllers Pod

$ sudo nsenter -t $(pidof kube-controllers) -n ip a
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
3: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether aa:da:6c:54:47:a7 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.219.64/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fd00:1:0:db4d:f2f2:402d:3b99:a180/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::a8da:6cff:fe54:47a7/64 scope link
       valid_lft forever preferred_lft forever

$ sudo nsenter -t $(pidof kube-controllers) -n ip -6 route
fd00:1:0:db4d:f2f2:402d:3b99:a180 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::ecee:eeff:feee:eeee dev eth0 metric 1024 pref medium

ノードの NAT テーブルの設定

Master ノードおよび Worker ノードの NAT テーブルは、次の内容となっていました。

$ sudo ip6tables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
cali-PREROUTING  all      ::/0                 ::/0                 /* cali:6gwbT8clXdHdC1b1 */
KUBE-SERVICES  all      ::/0                 ::/0                 /* kubernetes service portals */

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
cali-OUTPUT  all      ::/0                 ::/0                 /* cali:tVnHkvAo15HuiPy0 */
KUBE-SERVICES  all      ::/0                 ::/0                 /* kubernetes service portals */

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
cali-POSTROUTING  all      ::/0                 ::/0                 /* cali:O3lYWMrLQYEMJtB5 */
KUBE-POSTROUTING  all      ::/0                 ::/0                 /* kubernetes postrouting rules */

Chain KUBE-MARK-MASQ (0 references)
target     prot opt source               destination
MARK       all      ::/0                 ::/0                 MARK or 0x4000

Chain KUBE-NODEPORTS (1 references)
target     prot opt source               destination

Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination
MASQUERADE  all      ::/0                 ::/0                 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000

Chain KUBE-PROXY-CANARY (0 references)
target     prot opt source               destination

Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-NODEPORTS  all      ::/0                 ::/0                 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

Chain cali-OUTPUT (1 references)
target     prot opt source               destination
cali-fip-dnat  all      ::/0                 ::/0                 /* cali:GBTAv2p5CwevEyJm */

Chain cali-POSTROUTING (1 references)
target     prot opt source               destination
cali-fip-snat  all      ::/0                 ::/0                 /* cali:Z-c7XtVd2Bq7s_hA */
cali-nat-outgoing  all      ::/0                 ::/0                 /* cali:nYKhEzDlr11Jccal */

Chain cali-PREROUTING (1 references)
target     prot opt source               destination
cali-fip-dnat  all      ::/0                 ::/0                 /* cali:r6XmIziWUJsdOK6Z */

Chain cali-fip-dnat (2 references)
target     prot opt source               destination

Chain cali-fip-snat (1 references)
target     prot opt source               destination

Chain cali-nat-outgoing (1 references)
target     prot opt source               destination
MASQUERADE  all      ::/0                 ::/0                 /* cali:Ir_z2t1P6-CxTDof */ match-set cali60masq-ipam-pools src ! match-set cali60all-ipam-pools dst


$ sudo ipset list cali60masq-ipam-pools
Name: cali60masq-ipam-pools
Type: hash:net
Revision: 6
Header: family inet6 hashsize 1024 maxelem 1048576
Size in memory: 1240
References: 1
Number of entries: 1
Members:
fd00:1::/48

$ sudo ipset list cali60all-ipam-pools
Name: cali60all-ipam-pools
Type: hash:net
Revision: 6
Header: family inet6 hashsize 1024 maxelem 1048576
Size in memory: 1240
References: 1
Number of entries: 1
Members:
fd00:1::/48

確認結果の整理

上記の確認結果から、次のようなネットワーク構成が作られていることがわかります。

f:id:ymstmsys:20200715151619p:plain

Pod やノードのネットワーク設定および Calico の IPAM ブロックの内容から、通信は次のように行われることがわかります。

  • Pod
    • 任意の宛先の通信は、eth0 NIC から "fe80::ecee:eeff:feee:eeee" (ノード上の接頭辞 cali から始まる仮想 NIC) 宛にパケットを送信する。
  • ノード
    • 自ノード上の Pod 宛の通信は、Pod に接続された仮想 NIC のリンク上で NDP で宛先を見つけて、イーサネットフレームを送信する。
    • 自ノードに割り当てられた IPAM ブロックのうち、Pod に割り当てられていない IP アドレス宛の通信のパケットは捨てる。
    • 他ノードに割り当てられた IPAM ブロック宛の通信は、ノード間のネットワークを通して、そのノードにパケットを送信する。
    • ノードネットワーク宛の通信は、ens32 NIC のリンク上で NDP で宛先を見つけて、イーサネットフレームを送信する。
    • 上記以外宛の通信は、ens32 NIC からルーター宛にパケットを送信する。

また、各ノードの NAT テーブルの内容から、次のように NAT が行われることがわかります。

  • cali-nat-outgoing チェーンによって、送信元が cali60masq-ipam-pools ("fd00:1::/48")、宛先が cali60all-ipam-pools ("fd00:1::/48") 以外のパケットは、IPマスカレードする。

IPv6 通信の動作確認

異なるノード上の Pod 間の通信

Master ノード上の Pod から、Worker ノード上の Pod への通信を確認します。

Master ノード上で次のように ping を実行すると、問題なく成功しました。

$ sudo nsenter -t $(pidof coredns) -n ping6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 -c 4
PING fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780(fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780) 56 data bytes
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=1 ttl=62 time=0.446 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=2 ttl=62 time=0.737 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=3 ttl=62 time=0.782 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=4 ttl=62 time=0.777 ms

--- fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3054ms
rtt min/avg/max/mdev = 0.446/0.685/0.782/0.141 ms

このとき、Worker ノード上での tcpdump の結果は次のようになっており、Pod CIDR の IP アドレス同士で通信できていることが確認できます。

$ sudo nsenter -t $(pidof coredns) -n tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
01:24:50.428052 IP6 fd00:1:0:db4d:f2f2:402d:3b99:a181 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 1, length 64
01:24:50.428106 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > fd00:1:0:db4d:f2f2:402d:3b99:a181: ICMP6, echo reply, seq 1, length 64
01:24:51.434440 IP6 fd00:1:0:db4d:f2f2:402d:3b99:a181 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 2, length 64
01:24:51.434518 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > fd00:1:0:db4d:f2f2:402d:3b99:a181: ICMP6, echo reply, seq 2, length 64
01:24:52.458973 IP6 fd00:1:0:db4d:f2f2:402d:3b99:a181 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 3, length 64
01:24:52.459059 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > fd00:1:0:db4d:f2f2:402d:3b99:a181: ICMP6, echo reply, seq 3, length 64
01:24:53.482470 IP6 fd00:1:0:db4d:f2f2:402d:3b99:a181 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 4, length 64
01:24:53.482544 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > fd00:1:0:db4d:f2f2:402d:3b99:a181: ICMP6, echo reply, seq 4, length 64

また、traceroute の結果は次のようになっており、Master ノードと Worker ノードをホップとして経由していることが確認できます。

$ sudo nsenter -t $(pidof coredns) -n traceroute6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780
traceroute to fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 (fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780) from fd00:1:0:db4d:f2f2:402d:3b99:a181, 30 hops max, 24 byte packets
 1  2001:db8::11 (2001:db8::11)  0.126 ms  0.054 ms  0.031 ms
 2  2001:db8::12 (2001:db8::12)  0.513 ms  0.311 ms  0.427 ms
 3  fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 (fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780)  0.366 ms  0.577 ms  0.449 ms

ノードから異なるノード上の Pod への通信

Master ノードから、Worker ノード上の Pod への通信を確認します。

Master ノード上で次のように ping を実行すると、問題なく成功しました。

$ ping6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 -c 4
PING fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780(fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780) 56 data bytes
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=1 ttl=63 time=0.393 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=2 ttl=63 time=0.867 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=3 ttl=63 time=0.750 ms
64 bytes from fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: icmp_seq=4 ttl=63 time=0.440 ms

--- fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3072ms
rtt min/avg/max/mdev = 0.393/0.612/0.867/0.202 ms

このとき、Worker ノード上での tcpdump の結果は次のようになっており、NAT されることなく通信できていることが確認できます。

$ sudo nsenter -t $(pidof coredns) -n tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
04:18:18.357217 IP6 2001:db8::11 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 1, length 64
04:18:18.357248 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > 2001:db8::11: ICMP6, echo reply, seq 1, length 64
04:18:19.382617 IP6 2001:db8::11 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 2, length 64
04:18:19.382662 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > 2001:db8::11: ICMP6, echo reply, seq 2, length 64
04:18:20.406083 IP6 2001:db8::11 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 3, length 64
04:18:20.406126 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > 2001:db8::11: ICMP6, echo reply, seq 3, length 64
04:18:21.429982 IP6 2001:db8::11 > fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780: ICMP6, echo request, seq 4, length 64
04:18:21.430011 IP6 fd00:1:0:ab7e:b4b6:dc9b:3ca1:a780 > 2001:db8::11: ICMP6, echo reply, seq 4, length 64

Pod から異なるノードへの通信

Master ノード上の Pod から、Worker ノードへの通信を確認します。

Master ノード上で次のように ping を実行すると、問題なく成功しました。

$ sudo nsenter -t $(pidof coredns) -n ping6 2001:db8::12 -c 4
PING 2001:db8::12(2001:db8::12) 56 data bytes
64 bytes from 2001:db8::12: icmp_seq=1 ttl=63 time=0.629 ms
64 bytes from 2001:db8::12: icmp_seq=2 ttl=63 time=0.431 ms
64 bytes from 2001:db8::12: icmp_seq=3 ttl=63 time=0.743 ms
64 bytes from 2001:db8::12: icmp_seq=4 ttl=63 time=0.791 ms

--- 2001:db8::12 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3067ms
rtt min/avg/max/mdev = 0.431/0.648/0.791/0.140 ms

このとき、Worker ノード上での tcpdump の結果は次のようになっており、送信元アドレスが Master ノードのIPアドレスに変換されていることが確認できます。

$ sudo tcpdump -i ens32 -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens32, link-type EN10MB (Ethernet), capture size 262144 bytes
01:42:19.970799 IP6 2001:db8::11 > 2001:db8::12: ICMP6, echo request, seq 1, length 64
01:42:19.970876 IP6 2001:db8::12 > 2001:db8::11: ICMP6, echo reply, seq 1, length 64
01:42:20.991366 IP6 2001:db8::11 > 2001:db8::12: ICMP6, echo request, seq 2, length 64
01:42:20.991437 IP6 2001:db8::12 > 2001:db8::11: ICMP6, echo reply, seq 2, length 64
01:42:22.016677 IP6 2001:db8::11 > 2001:db8::12: ICMP6, echo request, seq 3, length 64
01:42:22.016780 IP6 2001:db8::12 > 2001:db8::11: ICMP6, echo reply, seq 3, length 64
01:42:23.041453 IP6 2001:db8::11 > 2001:db8::12: ICMP6, echo request, seq 4, length 64
01:42:23.041561 IP6 2001:db8::12 > 2001:db8::11: ICMP6, echo reply, seq 4, length 64

5. Service を用いた IPv6 ネットワークの確認

Service を用いる場合 IPv6 ネットワークの設定や動作についても確認していきます。

サンプルアプリケーションのデプロイ

Web アプリケーションを想定し、Apache HTTP Server のコンテナイメージを使って次の内容の Deployment を作成しました。
Master ノードと Worker ノードで1つずつ Pod が作成されるようにしています。

kind: Deployment
apiVersion: apps/v1
metadata:
  name: httpd
spec:
  replicas: 2
  selector:
    matchLabels:
      app: httpd
  template:
    metadata:
      labels:
        app: httpd
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
      topologySpreadConstraints:
      - topologyKey: kubernetes.io/hostname
        maxSkew: 1
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: httpd
      containers:
        - name: httpd
          image: httpd:latest
          ports:
            - containerPort: 80

また、次の内容の Service も作成し、Service 経由で Pod にアクセスできるようにしました。
1つの Service では1つのIPスタックしか扱えないため、デュアルスタック構成の場合は .spec.ipFamily に "IPv4" または "IPv6" を指定する必要があります。

kind: Service
apiVersion: v1
metadata:
  name: httpd
spec:
  type: NodePort
  ipFamily: IPv6
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: httpd

作成した Kubernetes リソースの状態

作成した Pod や Service には、期待通りに IPv6 アドレスが割り当てられました。

$ kubectl get deploy -o wide
NAME    READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES         SELECTOR
httpd   2/2     2            2           13m   httpd        httpd:latest   app=httpd

$ kubectl get pod -o wide
NAME                    READY   STATUS    RESTARTS   AGE   IP              NODE     NOMINATED NODE   READINESS GATES
httpd-9bdf85554-n2f55   1/1     Running   0          13m   10.244.171.65   worker   <none>           <none>
httpd-9bdf85554-nqnr4   1/1     Running   0          13m   10.244.219.66   master   <none>           <none>

$ kubectl get pod -o custom-columns=NAME:.metadata.name,IPv4:.status.podIPs[0].ip,IPv6:.status.podIPs[1].ip
NAME                    IPv4            IPv6
httpd-9bdf85554-n2f55   10.244.171.65   fd00:1:0:ab7e:b4b6:dc9b:3ca1:a781
httpd-9bdf85554-nqnr4   10.244.219.66   fd00:1:0:db4d:f2f2:402d:3b99:a182

$ kubectl get service -o wide
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE    SELECTOR
httpd        NodePort    fd00::9bc    <none>        80:31837/TCP   9s     app=httpd
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP        115m   <none>

NAT テーブルの設定

Master ノードおよび Worker ノードの NAT テーブルは、次の内容に変更されていました。

$ sudo ip6tables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
cali-PREROUTING  all      ::/0                 ::/0                 /* cali:6gwbT8clXdHdC1b1 */
KUBE-SERVICES  all      ::/0                 ::/0                 /* kubernetes service portals */

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
cali-OUTPUT  all      ::/0                 ::/0                 /* cali:tVnHkvAo15HuiPy0 */
KUBE-SERVICES  all      ::/0                 ::/0                 /* kubernetes service portals */

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
cali-POSTROUTING  all      ::/0                 ::/0                 /* cali:O3lYWMrLQYEMJtB5 */
KUBE-POSTROUTING  all      ::/0                 ::/0                 /* kubernetes postrouting rules */

Chain KUBE-MARK-MASQ (4 references)
target     prot opt source               destination
MARK       all      ::/0                 ::/0                 MARK or 0x4000

Chain KUBE-NODEPORTS (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  tcp      ::/0                 ::/0                 /* default/httpd: */ tcp dpt:31837
KUBE-SVC-6SDXOZXL2M4BHO2Q  tcp      ::/0                 ::/0                 /* default/httpd: */ tcp dpt:31837

Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination
MASQUERADE  all      ::/0                 ::/0                 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000

Chain KUBE-PROXY-CANARY (0 references)
target     prot opt source               destination

Chain KUBE-SEP-JYKFFQKKRMMGYAWD (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all      fd00:1:0:ab7e:b4b6:dc9b:3ca1:a781  ::/0                 /* default/httpd: */
DNAT       tcp      ::/0                 ::/0                 /* default/httpd: */ tcp to:[fd00:1:0:ab7e:b4b6:dc9b:3ca1:a781]:80

Chain KUBE-SEP-VATU5ZPQYFRGI5HR (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all      fd00:1:0:db4d:f2f2:402d:3b99:a182  ::/0                 /* default/httpd: */
DNAT       tcp      ::/0                 ::/0                 /* default/httpd: */ tcp to:[fd00:1:0:db4d:f2f2:402d:3b99:a182]:80

Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-MARK-MASQ  tcp     !fd00:1::/48          fd00::9bc            /* default/httpd: cluster IP */ tcp dpt:80
KUBE-SVC-6SDXOZXL2M4BHO2Q  tcp      ::/0                 fd00::9bc            /* default/httpd: cluster IP */ tcp dpt:80
KUBE-NODEPORTS  all      ::/0                 ::/0                 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

Chain KUBE-SVC-6SDXOZXL2M4BHO2Q (2 references)
target     prot opt source               destination
KUBE-SEP-JYKFFQKKRMMGYAWD  all      ::/0                 ::/0                 /* default/httpd: */ statistic mode random probability 0.50000000000
KUBE-SEP-VATU5ZPQYFRGI5HR  all      ::/0                 ::/0                 /* default/httpd: */

Chain cali-OUTPUT (1 references)
target     prot opt source               destination
cali-fip-dnat  all      ::/0                 ::/0                 /* cali:GBTAv2p5CwevEyJm */

Chain cali-POSTROUTING (1 references)
target     prot opt source               destination
cali-fip-snat  all      ::/0                 ::/0                 /* cali:Z-c7XtVd2Bq7s_hA */
cali-nat-outgoing  all      ::/0                 ::/0                 /* cali:nYKhEzDlr11Jccal */

Chain cali-PREROUTING (1 references)
target     prot opt source               destination
cali-fip-dnat  all      ::/0                 ::/0                 /* cali:r6XmIziWUJsdOK6Z */

Chain cali-fip-dnat (2 references)
target     prot opt source               destination

Chain cali-fip-snat (1 references)
target     prot opt source               destination

Chain cali-nat-outgoing (1 references)
target     prot opt source               destination
MASQUERADE  all      ::/0                 ::/0                 /* cali:Ir_z2t1P6-CxTDof */ match-set cali60masq-ipam-pools src ! match-set cali60all-ipam-pools dst

確認結果の整理

サンプルアプリケーションをデプロイする前と比較して、NAT テーブルには次の3つのチェーンが増えています。

  • KUBE-SEP-JYKFFQKKRMMGYAWD
  • KUBE-SEP-VATU5ZPQYFRGI5HR
  • KUBE-SVC-6SDXOZXL2M4BHO2Q

NAT テーブルの設定を読み解くと、ClusterIP "fd00::9bc" の TCP 80 番ポートへの通信では、次のように NAT が行われることがわかります。

  • 50% ずつの確率で KUBE-SEP-JYKFFQKKRMMGYAWD または KUBE-SEP-VATU5ZPQYFRGI5HR チェーンを実行する。
    • KUBE-SERVICES チェーン2行目
    • KUBE-SVC-6SDXOZXL2M4BHO2Q チェーン
  • KUBE-SEP-JYKFFQKKRMMGYAWD チェーンが実行されると、宛先を Worker ノードの Pod "fd00:1:0:ab7e:b4b6:dc9b:3ca1:a781" の TCP 80 番ポートに変換する。
    • KUBE-SEP-JYKFFQKKRMMGYAWD チェーン2行目
  • KUBE-SEP-VATU5ZPQYFRGI5HR チェーンが実行されると、宛先を Master ノードの Pod "fd00:1:0:db4d:f2f2:402d:3b99:a182" の TCP 80 番ポートに変換する。
    • KUBE-SEP-VATU5ZPQYFRGI5HR チェーン2行目
  • Pod CIDR "fd00:1::/48" 以外から ClusterIP への通信では、送信元をIPマスカレードする。
    • KUBE-SERVICES チェーン1行目
    • KUBE-MARK-MASQ チェーン
    • KUBE-POSTROUTING チェーン
  • Pod が送信元で、宛先が自 Pod に変換される場合、送信元をIPマスカレードする。
    • KUBE-SEP-JYKFFQKKRMMGYAWD チェーン1行目または KUBE-SEP-VATU5ZPQYFRGI5HR チェーン1行目
    • KUBE-MARK-MASQ チェーン
    • KUBE-POSTROUTING チェーン

このうち、最後の動作はいわゆるヘアピン NAT であり、Pod が ClusterIP 経由でのアクセスに対する応答を適切に返せるようにするために必要なものです。

また、NodePort への通信では、次のように NAT が行われることもわかります。

  • 送信元をIPマスカレードする。
    • KUBE-NODEPORTS チェーン1行目
    • KUBE-MARK-MASQ チェーン
    • KUBE-POSTROUTING チェーン
  • ClusterIP への通信と同じ NAT を行う。
    • KUBE-NODEPORTS チェーン2行目

なお、IPv4 の NAT テーブルには、サンプルアプリケーションに関する NAT 設定は追加されていませんでした。
Service の .spec.ipFamily で IPv6 を指定すると、IPv6 に関する NAT 設定のみ追加されることが確認できました。


6. まとめ

本記事では、IPv4-IPv6 デュアルスタック構成の Kubernetes クラスタを構築する方法と、IPv6 通信がどのように行われるようになっているかを解説しました。

本記事執筆時点では、Kubernetes クラスタをいったん IPv4 シングルスタック構成で構築すると、デュアルスタック構成に変更することはできません。
クラスタを新しく構築する際には、初めからデュアルスタック構成を設計して構築しておくと、後から困ることが少なくて良いでしょう。


7. 補足

ユニークローカルユニキャストアドレスの利用について

本記事では Pod CIDR にユニークローカルユニキャストアドレスを利用し、Pod から Pod CIDR 外に通信する際は、ノードでIPマスカレードを行うようにしました。
(calico-node の環境変数 CALICO_IPV6POOL_NAT_OUTGOING の指定を追加しました。)

ノードで NAT することで、ユニークローカルユニキャストアドレスが外部ネットワークに出ていかないようにするだけでなく、Pod CIDR に関する経路情報をルーターなどクラスタ外の機器に持たせなくても済むようにしています。

しかし、IPv6 ではアドレス空間が非常に広いため、内部ネットワークでもグローバルユニキャストアドレスを利用し、NAT せずに利用することが一般的です。

本記事では試していませんが、BGP ルーターを用意し、Pod CIDR に関する経路情報をルーターに渡して適切にルーティングされるようにすれば、NAT が不要になります。
NAT を行わない分だけ通信が高速化する効果も得られると考えられるため、興味がある方はお試しください。
https://docs.projectcalico.org/reference/resources/bgppeer

Kubernetes のクラスタ基盤部分での IPv6 の利用について

Kubernetes は多数のコンポーネント (etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kube-proxy など) で構成されており、それらは互いに通信して協調して動作しています。
本記事ではこの部分の通信は IPv4 で行われるようにしましたが、IPv6 で行うようにすることもできます。

具体的には、kubeadm を用いて Master ノードをセットアップする際、コマンドを次のように変更すると良いです。

$ sudo kubeadm init \
    --feature-gates="IPv6DualStack=true" \
    --pod-network-cidr="fd00:1::/48,10.244.0.0/16" \
    --service-cidr="fd00::/108,10.96.0.0/12" \
    --apiserver-advertise-address="2001:db8::11"
  • --apiserver-advertise-address パラメータを追加し、ノードの IPv6 アドレスを指定する。
  • --pod-network-cidr, --service-cidr パラメータは、IPv6 アドレスを先に記載する。
    • IPv6 をプライマリ、IPv4 をセカンダリとする。

これにより、etcd や kube-apiserver の通信は IPv6 で行われるようになります。

ただし、kubeadm は kube-controller-manager や kube-scheduler を IPv4 アドレスにバインドして実行するような構成でクラスタを構築するようになっており、その動作を変更するパラメータは用意されていません。
そのため、kubeadm でクラスタを構築したのち、それらの静的 Pod のマニフェストを手動で変更し、IPv6 が利用されるようにする必要があります。
https://github.com/kubernetes/kubernetes/blob/v1.18.5/cmd/kubeadm/app/phases/controlplane/manifests.go#L306