PodのNICにElasticIPを直接持たせる¶
ElasticIPAttachmentを使うと、Vpc内にいるPodのアドレスとElasticIPの間を、Nodeが1:1 NATで変換します。Podの中から見えるのはVpc内のアドレスです。
PodのNICにElasticIPのアドレスそのものを持たせることもできます。Podの中でip addrを実行すると外部アドレスが見え、パケットはNATを通りません。ペイロードに自分のアドレスを書くプロトコルを動かすときや、ゲストOSに自分の外部アドレスを見せたいVMで使います。
このガイドでは、eth0にElasticIPを持たせたPodと、追加NICにElasticIPを持たせたPodを作り、外部から到達することを確認します。
このガイドで構築するもの¶
- ElasticIP
web-eipをeth0に持つPodweb - default Subnetのeth0と、ElasticIP
gw-eipを持つeth1のPodgw - クラスター外から両方のElasticIPへ
curlで到達確認
前提条件¶
- ExternalNetworkが構築済みであること。本ガイドではBGPを使ってExternalNetworkを構築するの手順1から5で作った
ext-net(10.225.51.0/24) を使います。ARPの場合はARPを使ってExternalNetworkを構築するの手順1と2で作ったext-net-arpに読み替えてください kubectl juneauプラグイン(確認とトラブルシュートに使います)
VpcやSubnet、RouteTableのinternetGatewayルートは要りません。ElasticIPを直接持つNICはどのVpcにも属さないからです。
NATとの違い¶
| ElasticIPAttachment (NAT) | NICに直接持たせる | |
|---|---|---|
| Podの中のアドレス | Vpc内のアドレス | ElasticIP (/32) |
| 置けるNIC | default以外のSubnetのNIC | eth0でも追加NICでも |
RouteTableのinternetGatewayルート | 必要 | 不要 |
| SecurityGroupとNetworkACL | 効く | 効かない |
| ClusterIP Service | Vpcの設定しだいで届く | 届かない |
| Serviceのバックエンド | なれる | eth0が直接持つPodはなれない |
kubectl juneau trace | 使える | 使えない |
1つのElasticIPを両方で同時に使うことはできません。
手順¶
1. ExternalNetworkのnetworkIDを確認¶
ElasticIPを直接持つNICは、ExternalNetworkのstatus.networkIDをVNIとするセグメントにいるものとして扱われます。0の間は、PodControllerがNICを作らずに待ちます。
$ kubectl get externalnetwork ext-net -o jsonpath='{.status.networkID}'
4097
詳細はExternalNetworkを参照してください。
2. ElasticIPを作成¶
apiVersion: juneau.loutres.me/v1alpha1
kind: ElasticIP
metadata:
name: web-eip
spec:
externalNetwork: ext-net
---
apiVersion: juneau.loutres.me/v1alpha1
kind: ElasticIP
metadata:
name: gw-eip
spec:
externalNetwork: ext-net
$ kubectl get elasticip
NAME EXTERNALNETWORK ADDRESS ATTACHMENTKIND ATTACHMENT PHASE ALLOCATED ATTACHED
gw-eip ext-net 10.225.51.6 Available True False
web-eip ext-net 10.225.51.8 Available True False
ElasticIPはPodと同じnamespaceに作ってください。別のnamespaceのElasticIPは参照できません。
3. eth0にElasticIPを持たせる¶
juneau.loutres.me/elastic-ip アノテーションにElasticIPの名前を書きます。
apiVersion: v1
kind: Pod
metadata:
name: web
annotations:
juneau.loutres.me/elastic-ip: web-eip
spec:
containers:
- name: nginx
image: nginx:1.27
readinessProbe:
httpGet:
path: /
port: 80
- name: tools
image: nicolaka/netshoot:v0.16
command: ["sleep", "infinity"]
juneau.loutres.me/networks に interface: eth0 のエントリを書いても同じです。
annotations:
juneau.loutres.me/networks: |
[
{"interface": "eth0", "elasticIP": "web-eip"}
]
どちらの書き方でも、eth0はdefault Subnetに入りません。juneau.loutres.me/subnet、juneau.loutres.me/address、juneau.loutres.me/security-groups と一緒に書くとwebhookが拒否します。networksでeth0を書いた場合は、juneau.loutres.me/elastic-ipも一緒に書けません。ElasticIPのエントリにはaddressとsecurityGroupsも書けません。
Podが起動したら、NetworkInterfaceとElasticIPを確認します。
$ kubectl get networkinterface web.eth0
NAME NODE SUBNET L2NETWORK ELASTICIP ADDRESS PHASE
web.eth0 worker-1 web-eip 10.225.51.8/32 Ready
$ kubectl get elasticip web-eip
NAME EXTERNALNETWORK ADDRESS ATTACHMENTKIND ATTACHMENT PHASE ALLOCATED ATTACHED
web-eip ext-net 10.225.51.8 NetworkInterface web.eth0 Attached True True
kubectl get pod web -o wide のIPもElasticIPのアドレスになります。
Podの中では、/32のアドレスと、169.254.0.1へ向かうデフォルトルートが見えます。
$ kubectl exec web -c tools -- ip -4 addr show eth0
2: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link-netnsid 0
inet 10.225.51.8/32 scope global eth0
valid_lft forever preferred_lft forever
$ kubectl exec web -c tools -- ip route
default via 169.254.0.1 dev eth0 onlink
169.254.0.1はどのホストのアドレスでもありません。このアドレス宛のARPに、Node側のvethでeBPFがvethのMACを返します。Podから出たパケットはNodeが受け取り、宛先に応じて転送します。
4. 外部からの疎通を確認¶
上流ルータ側、または上流ルータから到達可能なホストから確認します。
$ curl -sS http://10.225.51.8/
<!DOCTYPE html>
...
<h1>Welcome to nginx!</h1>
Podから外へ出るパケットの送信元も、ElasticIPのままです。
$ kubectl exec web -c tools -- curl -sS https://1.1.1.1/cdn-cgi/trace | grep ip=
ip=10.225.51.8
5. 追加NICにElasticIPを持たせる¶
eth0はこれまで通りSubnetに置いて、外部アドレスだけを追加NICで持たせることもできます。eth0がSubnetにあるので、クラスターDNSやServiceはeth0から使えます。
apiVersion: v1
kind: Pod
metadata:
name: gw
annotations:
juneau.loutres.me/networks: |
[
{"interface": "eth1", "elasticIP": "gw-eip"}
]
spec:
containers:
- name: nginx
image: nginx:1.27
- name: tools
image: nicolaka/netshoot:v0.16
command: ["sleep", "infinity"]
$ kubectl get networkinterface gw.eth0 gw.eth1
NAME NODE SUBNET L2NETWORK ELASTICIP ADDRESS PHASE
gw.eth0 worker-2 default 10.16.0.9/16 Ready
gw.eth1 worker-2 gw-eip 10.225.51.6/32 Ready
追加NICのデフォルトルートは、mainテーブルではなく専用のテーブルに入ります。送信元がElasticIPのパケットだけが、ルールでそのテーブルへ向かいます。
$ kubectl exec gw -c tools -- ip rule
0: from all lookup local
100: from 10.225.51.6 lookup 182530822
32766: from all lookup main
32767: from all lookup default
$ kubectl exec gw -c tools -- ip route show table 182530822
default via 169.254.0.1 dev eth1 onlink
テーブルの番号は、ElasticIPのアドレスを32ビットの整数として読んだ値です。決め方はNetworkInterfaceのルートテーブルの番号を参照してください。
外から10.225.51.6に届いた通信への応答は、送信元が10.225.51.6なのでeth1から出ます。
$ curl -sS http://10.225.51.6/
<!DOCTYPE html>
...
Podの中から新しく張る通信は、何も指定しなければmainテーブルのデフォルトルートでeth0から出ます。ElasticIPから出したいときは、送信元のアドレスを指定してください。
$ kubectl exec gw -c tools -- curl -sS --interface 10.225.51.6 https://1.1.1.1/cdn-cgi/trace | grep ip=
ip=10.225.51.6
type: arpのExternalNetworkで使う¶
ElasticIP controllerがeip-<namespace>-<ElasticIP名>のARPAdvertisementを作り、アドレスを持っているNetworkInterfaceのspec.nodeNameをNodeに書きます。PodのいるNodeがARPに応答します。
$ kubectl get arpadvertisement eip-default-web-eip
NAME EXTERNALNETWORK ADDRESS NODE AGE
eip-default-web-eip ext-net-arp 10.225.32.240 worker-1 30s
NODEはkubectl get networkinterface web.eth0のNODEと一致します。PodのNodeが変わるとARPAdvertisementも追従しますが、gratuitous ARPは送りません。上流のneighborキャッシュが更新されるまで、外からの通信は古いNodeに届きます。詳しくはARPを使ってExternalNetworkを構築するの制約を参照してください。
type: bgpのExternalNetworkで使う¶
ElasticIPごとの経路は広報しません。BGPAdvertisementで広報しているAddressPoolの経路がそのまま使われます。上流ルータがECMPでどのNodeに送っても、そのNodeがPodのNodeへVXLANで運びます。VNIはExternalNetworkのstatus.networkIDです。
AddressPoolがBGPAdvertisementで広報されていないと、外からは届きません。
Podから出ていくパケット¶
ElasticIPを持つNICから出たパケットは、SNATもVpcのRouteTableも通りません。宛先によって次のように扱われます。
| 宛先 | 扱い |
|---|---|
Nodeのexternal_address_poolsにEXTERNAL_ADDRESS_DELIVERED_HEREで入っているアドレス | Nodeの中で折り返し、外からNodeに届いたパケットと同じ処理に回します |
| PodがいるNodeのアドレス | Nodeのカーネルに渡します |
| それ以外 | PodがいるNodeのルーティングテーブルに従って、NodeのNICから出します |
EXTERNAL_ADDRESS_DELIVERED_HEREに入るのは、そのNodeが外から受け取るアドレスです。他のPodが直接持つElasticIPは、PodがどのNodeにいても全Nodeに入るので、Pod同士はNodeの中で折り返して届きます。ServiceLoadBalancerのVIPやNATGatewayのアドレスが入るかどうかは、そのNodeが広報やARPの応答を受け持っているかで決まります。入っていなければNodeのNICから出て、外側のネットワークを通って受け持つNodeに届きます。
spec.nodeNameで別のNodeに絞ったBGPAdvertisementの範囲はEXTERNAL_ADDRESS_DELIVERED_ELSEWHEREになり、全Nodeが広報するAddressPoolに含まれていても折り返しません。NATGatewayがNodeごとに広報するアドレスがこれにあたります。
VpcのPodが持つVpc内のアドレスには届きません。ClusterIP Serviceにも届きません。
送信元がElasticIPでないパケットと、IPv4以外のパケットは捨てます。ARPは169.254.0.1宛のリクエストと、自分のElasticIPについての応答だけを通します。
注意点¶
DNS¶
eth0がElasticIPを持つPodは、dnsPolicyがDefaultに書き換わります。ClusterFirstが指すCoreDNSのClusterIPに、ElasticIPのNICからは届かないからです。
| 作成時のdnsPolicy | 結果 |
|---|---|
未指定、ClusterFirst、ClusterFirstWithHostNet | Default |
Default | そのまま |
None | そのまま |
DefaultのPodは、Nodeのresolv.conf(kubeletの--resolv-conf)にあるDNSサーバを使います。問い合わせの送信元はElasticIPなので、そのDNSサーバがElasticIPからの問い合わせに答え、応答を返せる必要があります。*.svc.cluster.localのようなクラスター内の名前は引けません。
このDNSサーバには、PodのアドレスからNodeの外を通って届く必要があります。kindのNodeのresolv.confが指すdockerの内蔵DNSは、Nodeのnetwork namespaceの中でだけ使える転送なので、Podからは届かず、名前解決ができません。
juneau.loutres.me/dns-inject-skip: "true"を付けたPodは、dnsPolicyを書き換えません。dnsPolicyを書いていなければClusterFirstのままになり、名前解決ができません。
eth0がSubnetで、追加NICだけがElasticIPを持つPodは、これまで通りSubnetのDNSが注入されます。
kubeletのプローブ¶
eth0がElasticIPを持つPodには、CNIがNodeに<ElasticIP>/32 dev <Podのvethのホスト側>の経路を入れます。kubeletのhttpGetやtcpSocketのプローブはこの経路でPodに届き、応答はPodからNodeのカーネルに戻ります。controllerの--enable-probe-rewriteは、このPodのプローブを書き換えません。
経路はPodのsandboxと一緒に消えます。追加NICのElasticIPには経路を入れません。kubeletがプローブするのはeth0のアドレスだからです。
Service¶
eth0がElasticIPを持つPodは、ServiceとServiceLoadBalancerのバックエンドになれません。selectorに一致しても、daemonはバックエンドに入れません。外に公開するならElasticIPのアドレスへ直接つないでください。
ElasticIPのNICからClusterIP Serviceにも届きません。Serviceを使いたいPodは、eth0をSubnetに置いて、ElasticIPは追加NICに持たせてください。
SecurityGroupとNetworkACL¶
ElasticIPを持つNICには、SecurityGroupもNetworkACLも効きません。どのVpcにも属さないからです。そのアドレスに届いたパケットは、ポートを問わずすべてPodに入ります。
守りたいポートがあるなら、Podの中でファイアウォールを設定してください。
ElasticIPを削除するとき¶
PodのNICがElasticIPを参照している間、ElasticIPの削除は完了しません。アドレスの予約を外すと、動いているPodのアドレスを別のElasticIPが取れてしまうからです。先にPodを消してください。
$ kubectl delete elasticip web-eip --wait=false
$ kubectl get elasticip web-eip -o jsonpath='{.status.conditions[?(@.type=="Allocated")].reason}'
WaitingForNetworkInterfaces
Podを作り直すとき¶
1つのElasticIPを持てるNICは1つだけです。持っているPodが動いている間に、同じElasticIPを参照する別のPodを作ると、webhookが拒否します。
次の場合は受け付けて、新しいPodのNICは古いNICが消えるまでPendingで待ちます。その間、新しいPodはContainerCreatingのままです。
- 持っているPodが終了中
- 持っているNICと新しいNICの
spec.allocationIdentityが同じ。KubeVirtのvirt-launcher Podはvmi.<VirtualMachine名>が入るので、VMを再起動しても同じElasticIPを持ち続けます
webhookはPodを作るときにNetworkInterfaceを見て判断するので、同じElasticIPを参照するPodを2つ同時に作ると、両方とも受け付けられることがあります。この場合も両方のNICが作られ、ElasticIPを持てなかった方はPendingで待ちます。reasonはWaitingForElasticIPで、messageにアドレスを持っているNICの名前が出ます。持っているPodを消すと、待っていたNICがすぐにアドレスを引き継ぎます。
Deploymentでは、ローリングアップデートが古いPodを残したまま新しいPodを作ろうとするので、webhookが新しいPodを拒否して更新が進みません。1つのElasticIPを使うDeploymentはreplicas: 1にして、strategy.type: RecreateかmaxSurge: 0を指定してください。古いPodが終了中になってから新しいPodが作られます。
制限¶
kubectl juneau traceは、ElasticIPを持つNICを送信元にも宛先にも指定できません。tracing such a NIC is not supported yetというエラーになります。このNICを通るパケットはtraceイベントを出しません- IPv4だけです
- MTUはSubnetのNICと同じくカーネルの既定値で、VXLANのオーバーヘッドを引いていません。BGPでPodのいないNodeに着いたパケットはVXLANで運ぶので、underlayのMTUに50バイトの余裕が無いと、大きなパケットが届きません
- KubeVirtのライブマイグレーションには対応していません。移行先のvirt-launcher PodのNICは移行元のNICが消えるまで
Pendingで待つので、移行先のPodが起動しません。また、VMのゲストOSは169.254.0.1のMACとして古いvethのMACを覚えています。Podが変わったあと、ゲストのARPキャッシュが更新されるまでは、Nodeのカーネル宛の通信(プローブの応答など)が届きません - ElasticIPAttachmentとPodを同時に作ってwebhookをすり抜けると、ElasticIPは
Errorになり、NICとElasticIPAttachmentはPendingになります。NICのmessageにはElasticIPAttachmentの名前が出ます。すでに動いていたPodのデータプレーンの設定は外れず、ElasticIPのアドレスで通信を続けます。ElasticIPAttachmentを消すと、NICはアドレスを取り戻し、まだ起動していなかったPodも起動します - Nodeが落ちてNetworkEndpointが残ったまま、同じElasticIPを持つPodが別のNodeで起動すると、2つのNetworkEndpointが同じアドレスを持ちます。
arp_tableとfdbは後から書いた方が勝つので、古いNetworkEndpointが消えるまで外からの通信が落ちたNodeに向かうことがあります - NodeのアドレスがAddressPoolの範囲に入っていると、PodからそのNodeへの応答がNodeのカーネルに渡らず、kubeletにプローブの応答が届きません。AddressPoolにNodeのアドレスを含めないでください
- Podのアノテーションの組み合わせを確認するwebhookは、Podの作成時にしか動きません。作成後に書き換えたアノテーションは確認されないので、NICを変えるときはPodを作り直してください
うまくいかないとき¶
kubectl get networkinterfaceにNICが出てこない- PodControllerは、ElasticIPが無い、ElasticIPの
ADDRESSが空、ExternalNetworkのstatus.networkIDが0、のどれかの間はNetworkInterfaceを作りません。controllerのログにwaiting for the network of a Pod NICと理由が出ます - ElasticIPがPodと同じnamespaceにあるか確認してください
- PodControllerは、ElasticIPが無い、ElasticIPの
- NetworkInterfaceが
Pendingのままkubectl get networkinterface web.eth0 -o jsonpath='{.status.conditions[?(@.type=="Allocated")]}'でreasonとmessageを見ます- reasonが
WaitingForElasticIPなら、messageに書かれたNetworkInterfaceかElasticIPAttachmentがElasticIPを使っています。古いPodが残っていないか、ElasticIPAttachmentを消し忘れていないか確認してください - ElasticIPのreasonが
WaitingForHandoverなら、削除中のNetworkInterfaceが消えるのを待っています。messageに名前が出ます。finalizerで止まっていないか確認してください
- NetworkInterfaceは
Allocatedなのに、PodがContainerCreatingのままkubectl describe pod webのイベントを見ます。Failed to read the ElasticIP of a NICがforbiddenで出ている場合、daemonのServiceAccountがElasticIPを読めません。ClusterRoleBindingcni-daemon-elasticip-viewer-bindingを含むdaemonのマニフェストを適用し直してください
-
外から届かない
- まず
kubectl juneau describe pod webで、NICの行がElasticIP web-eip (direct, ..., carried by this NIC)になっているか見ます。carried by NetworkInterface ...なら別のNICが持っています -
全Nodeの
elastic_ip_directにアドレスが入っているか確認します。NETWORK_IDはExternalNetworkのstatus.networkIDと同じはずです。入っていないNodeに届いたパケットは捨てられます$ kubectl juneau bpf dump elastic_ip_direct --filter addr=10.225.51.8 NODE ADDR NETWORK_ID worker-1 10.225.51.8 4097 worker-2 10.225.51.8 4097 -
同じく全Nodeの
external_address_poolsに、そのアドレスの/32がEXTERNAL_ADDRESS_DELIVERED_HEREで入っているか確認します。ここに無いアドレス宛のパケットは、NodeのNICに着いた時点で捨てられます$ kubectl juneau bpf dump external_address_pools --filter addr=10.225.51.8 NODE PREFIXLEN ADDR DELIVERY worker-1 32 10.225.51.8 EXTERNAL_ADDRESS_DELIVERED_HERE worker-2 32 10.225.51.8 EXTERNAL_ADDRESS_DELIVERED_HERE -
全Nodeの
arp_tableで、SUBNET_IDがnetworkID、IPADDRがElasticIPのエントリのMACが、Podのeth0のMACになっているか確認します。PodのMACはkubectl get networkendpoint web.eth0 -o jsonpath='{.spec.macAddress}'で分かります$ kubectl juneau bpf dump arp_table --filter subnet_id=4097 --filter ipaddr=10.225.51.8 -
fdbを--filter subnet_id=4097で見ます。PodのいるNodeではIFINDEXにvethのifindexが、それ以外のNodeではIFINDEXが0でVTEP_IPにPodのNodeのアドレスが入っているはずです -
PodのいるNodeの
ifindex_external_networkに、vethのifindexとElasticIPが入っているか確認します。無いと、そのNICから出るパケットはすべて捨てられます$ kubectl juneau bpf dump ifindex_external_network --node worker-1 IFINDEX NETWORK_ID IPV4 23 4097 10.225.51.8 -
type: arpなら、kubectl get arpadvertisement eip-default-web-eipのNODEがPodのNodeか、そのNodeのkubectl juneau bpf dump external_arp_tableにアドレスがあるかを見ます type: bgpなら、AddressPoolがBGPAdvertisementで広報されているか、kubectl get bgpnodestateがReadyかを見ます- Podから外に出られない
- Podの中で
ip neigh show 169.254.0.1を実行し、MACがkubectl juneau bpf dump ifindex_host_mac --node worker-1 --filter ifindex=23の値と同じか確認します。ARPが解決できていなければ、ifindex_external_networkかifindex_host_macにvethが入っていません - Podの中で別のアドレスを振っていたり、VMのゲストOSでNATしていたりすると、送信元がElasticIPでないパケットになり、捨てられます
- 追加NICの場合、送信元をElasticIPにしないとeth0から出ます。
kubectl exec gw -c tools -- ip route get 1.1.1.1 from 10.225.51.6でdev eth1になるか見てください - readinessProbeが失敗する
- PodのいるNodeで
ip route get 10.225.51.8を実行し、dev eth0+で始まるvethが返るか見ます。返らなければNodeの経路が入っていません。Podを作り直してください - NodeのアドレスがAddressPoolの範囲に入っていないか確認してください。入っていると、Podからの応答がNodeのカーネルではなく外部アドレスの処理に回ります
- 他のPodのElasticIPやServiceLoadBalancerのVIPに届かない
- 送信元PodのNodeで
kubectl juneau bpf dump external_address_pools --node <Node名>を見ます。宛先がEXTERNAL_ADDRESS_DELIVERED_HEREの範囲に入っていればNodeの中で折り返します。EXTERNAL_ADDRESS_DELIVERED_ELSEWHEREなら、別のNodeが広報しているアドレスとして上流ルータへ出します。どちらにも無ければ、NodeのNICから外へ出します
- まず