ARPを使ってExternalNetworkを構築する¶
type: arpのExternalNetworkでは、外部アドレス宛のARP RequestにNodeが直接ARP Replyを返します。上流とBGPセッションを張れない環境でも、NodeのNICと同じL2セグメントからアドレスを切り出せば、Podに外部到達可能なIPアドレスを割り当てられます。このガイドはその一連のリソースをゼロから組み立てる手順を示します。
BGPで経路を広報する場合はBGPを使ってExternalNetworkを構築するを参照してください。
このガイドで構築するもの¶
- NodeのNICと同じL2サブネットから切り出したアドレス範囲
10.225.32.240-10.225.32.250を持つAddressPool type: arpのExternalNetwork。BGPPeerもBGPAdvertisementも作りません- EIP対象Pod用の専用Vpc/Subnet (
10.60.0.0/24) とInternetGateway経路 - ElasticIPをnginx Podに割り当て、同じL2上のホストから
curl http://<elasticIP>/で到達確認
前提条件¶
- Juneauのcontroller/daemonが動作しているクラスター
- NodeのNICが接続しているL2サブネット(本ガイドでは
10.225.32.0/24) - そのサブネットのうち、DHCPや他のホストに使われていないアドレス範囲(本ガイドでは
10.225.32.240から10.225.32.250) - daemonの
--node-ingress-ifaceが外部L2に面したNICを指していること。未指定の場合はNodeのInternalIPを持つNICが使われます
制約¶
arp modeには、BGPを使う場合には無い制約があります。構築を始める前に確認してください。
外部アドレスはNodeのNICと同じL2サブネット内であること。 ARPはブロードキャストドメインの中でしか届きません。ルータを1つ挟んだ別サブネットのアドレスを払い出しても、そのアドレスへのARP RequestはNodeまで来ないので、誰も応答しません。
--node-ingress-ifaceが外部L2に面したNICであること。 daemonのeBPFプログラムはこのNICのingressでARP Requestを受け取り、同じNICへReplyを折り返します。overlay用のNICや管理用のNICを指していると、外部からのARP Requestがそもそもプログラムに届きません。
1つのアドレスには1つのNodeしか応答しません。 上流から見た宛先MACは1つだけなので、BGPのECMPのようにNode間でトラフィックを分散させることはできません。1アドレスあたりの帯域はNode1台分です。ServiceLoadBalancerのbackendが複数Nodeに散っていても、外から入る経路は1本になります。
応答Nodeが移ってもgratuitous ARPを送りません。 arp modeで一番影響が大きい制約です。ElasticIPを付けたPodが別のNodeへ再スケジュールされたとき、あるいはServiceLoadBalancerの応答Nodeが切り替わったとき、Juneauは新しいMACを上流に通知しません。上流のneighborエントリがaging outするまで通信は戻らず、Linuxのルータで概ね数十秒、L2スイッチや商用ルータではさらに長くかかることがあります。
急ぐなら、上流側でip neigh flush <アドレス>を実行してキャッシュを捨ててください。 Juneau側からGARPを送る余地はあります。virtserviceがすでにAF_PACKETでフレームを送出する仕組みを持っているので、同じものをnode ingressのNICに向ければeBPFを変えずに実装できます。現時点では入っていません。
ARP Replyに載せるMACは物理NICのMACです。 アドレスごとの仮想MACは使いません。応答Nodeが変わるとMACも変わるので、上流のキャッシュが更新されるまで待つことになります。
AddressPoolの範囲にNodeのInternalIPを含めないでください。 含めたAddressPool自体は作成できてしまいますが、そのアドレスがElasticIPなどに払い出された時点で、ARPAdvertisementの作成がwebhookに拒否されます。仮に応答してしまうと、Node自身がクラスター外から見えなくなります。
手順¶
1. AddressPoolを作成¶
外部に出すアドレス範囲をstart-end形式で定義します。
apiVersion: juneau.loutres.me/v1alpha1
kind: AddressPool
metadata:
name: ext-pool-arp
spec:
advertiseMode: arp
addresses:
- 10.225.32.240-10.225.32.250
advertiseMode: arpではCIDRを書けません。advertiseModeは変更不可です。詳細はAddressPoolを参照してください。
controllerがaddr-ext-pool-arpという名前のAllocationPoolを作ります。範囲が正しく渡っているか確認します。
$ kubectl get allocationpool addr-ext-pool-arp -o yaml
...
spec:
type: ip
strategy: firstFit
ip:
ranges:
- start: 10.225.32.240
end: 10.225.32.250
2. ExternalNetworkを作成¶
AddressPoolを1つの論理的な外部ネットワークとしてまとめます。
apiVersion: juneau.loutres.me/v1alpha1
kind: ExternalNetwork
metadata:
name: ext-net-arp
spec:
type: arp
addressPools:
- ext-pool-arp
type: arpの場合、参照するAddressPoolはadvertiseMode: arpである必要があります。詳細はExternalNetworkを参照してください。
BGPを使う場合と違い、ここでBGPPeerやBGPAdvertisementを作る手順はありません。
3. EIP対象Pod用のVpc/Subnetを作成¶
ElasticIPを付与するPodは、default以外のSubnetに配置する必要があります。専用Vpcとそこに属するSubnetを用意します。
apiVersion: juneau.loutres.me/v1alpha1
kind: Vpc
metadata:
name: ext-vpc
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
name: ext-subnet
spec:
vpc: ext-vpc
cidr: 10.60.0.0/24
4. RouteTableにInternetGatewayルートを追加¶
VpcのメインRouteTableは自動生成されますが、EIP egress (Pod → 外部) に必要なInternetGateway向けデフォルトルートはデフォルトでは含まれません。手動で追記します。
apiVersion: juneau.loutres.me/v1alpha1
kind: RouteTable
metadata:
name: ext-vpc
spec:
vpc: ext-vpc
routes:
- dst: 0.0.0.0/0
via:
type: internetGateway
RouteTableのメタ名はVpc名と同じです。Vpcと同じマニフェストにまとめて適用するなら、kubectl apply --server-sideを使ってください。詳細はRouteTableを参照してください。
5. ElasticIPを作成¶
ExternalNetworkから1つのアドレスを払い出します。
apiVersion: juneau.loutres.me/v1alpha1
kind: ElasticIP
metadata:
name: nginx-eip
spec:
externalNetwork: ext-net-arp
$ kubectl get elasticip nginx-eip
NAME EXTERNALNETWORK ADDRESS ATTACHMENT PHASE ALLOCATED ATTACHED
nginx-eip ext-net-arp 10.225.32.240 Available True False
PHASE: AvailableでADDRESSが埋まればアドレス確保完了です。この時点ではまだどのNodeも応答しません。応答するNodeはElasticIPAttachmentで決まるためです。
6. Podをデプロイ¶
対象のPodを、手順3で作ったSubnetに配置します。juneau.loutres.me/subnet annotationで明示します。
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
annotations:
juneau.loutres.me/subnet: ext-subnet
spec:
containers:
- name: nginx
image: nginx:1.27
NetworkInterface名はデフォルトで <Pod名>.eth0 です。
7. ElasticIPAttachmentでひもづけ¶
ElasticIPをPodのNetworkInterfaceに関連付けます。
apiVersion: juneau.loutres.me/v1alpha1
kind: ElasticIPAttachment
metadata:
name: nginx-eip-attach
spec:
elasticIPRef:
name: nginx-eip
targetRef:
networkInterfaceName: nginx.eth0
$ kubectl get elasticipattachment
NAME ELASTICIP NETWORKINTERFACE EIP PODIP NODE PHASE READY
nginx-eip-attach nginx-eip nginx.eth0 10.225.32.240 10.60.0.12 worker-1 Attached True
PHASE: AttachedかつREADY: Trueになれば、該当NodeでElasticIPがPodに関連付けられた状態です。詳細はElasticIPAttachmentを参照してください。
8. ARPAdvertisementを確認¶
ElasticIP controllerが、応答するNodeを指定したARPAdvertisementを作ります。
$ kubectl get arpadvertisement
NAME EXTERNALNETWORK ADDRESS NODE AGE
eip-default-nginx-eip ext-net-arp 10.225.32.240 worker-1 10s
NODEが手順7のNODEと一致していることを確認します。ここがElasticIPAttachmentのstatus.nodeNameをそのまま反映します。
Node上のdaemonがこの内容をeBPFのmapに落とします。実際に入った内容は次のコマンドで見られます。
$ kubectl juneau bpf dump external_arp_table
詳細はARPAdvertisementを参照してください。
9. 外部からの疎通を確認¶
同じL2サブネット上の任意のホストから、まずARPで解決できるかを見ます。
$ arping -c 3 -I eth0 10.225.32.240
ARPING 10.225.32.240 from 10.225.32.5 eth0
Unicast reply from 10.225.32.240 [02:42:0a:e1:20:03] 0.712ms
返ってきたMACは、応答Node (worker-1) の外部NICのMACです。他のNodeのMACが返る場合や、複数のMACが返る場合は設定を疑ってください。
続いてHTTPで到達を確認します。
$ curl -sS http://10.225.32.240/
<!DOCTYPE html>
...
<h1>Welcome to nginx!</h1>
レスポンスが返れば、ARP解決 → NodeへのL2転送 → ElasticIPのDNAT → Pod、の経路が通っています。
NATGatewayとServiceLoadBalancerで使う¶
同じExternalNetworkをNATGatewayとServiceLoadBalancerからも参照できます。ARPAdvertisementの作られ方だけが異なります。
NATGatewayを作ると、NodeごとにExternalNetworkAttachmentが1つずつ作られ、それぞれが別のアドレスを1つ消費します。各ExternalNetworkAttachmentが自分のNodeを指すARPAdvertisementを作るので、Node数だけアドレスが必要です。
ServiceLoadBalancerでは、status.advertisingNodesのうち1つだけがVIPに応答します。選ばれたNodeはstatus.arpAnnouncingNodeで確認できます。
$ kubectl get slb
NAME SERVICE EXTERNALNETWORK VIP PHASE ADVERTISINGNODES ARPNODE ALLOCATED AVAILABLE
web web ext-net-arp 10.225.32.241 Ready 2 worker-2 True True
ADVERTISINGNODESが2でもARPNODEは1つです。上流から見た入口はこの1Nodeだけになります。
うまくいかないとき¶
- ElasticIPの
ADDRESSが埋まらないPHASE: Errorなら、AddressPoolのadvertiseModeとExternalNetworkのtypeが食い違っています。どちらもarpに揃えてくださいPHASE: Pendingのままなら、AddressPoolの範囲を使い切っている可能性があります。kubectl get allocationleaseで払い出し済みのアドレスを確認できます
kubectl get arpadvertisementにエントリが無い- ElasticIPの場合、ElasticIPAttachmentが
PHASE: Attachedになっているか。外れている間は意図的に削除されます - ServiceLoadBalancerの場合、
status.advertisingNodesが空でないか。Local backendが1つも無ければ応答するNodeも決まりません
- ElasticIPの場合、ElasticIPAttachmentが
- arpingが返ってこない
- クライアントがNodeと同じL2サブネットにいるか。ルータを挟んでいると届きません
- 応答するはずのNodeで
kubectl juneau bpf dump external_arp_tableを実行し、期待するアドレスが入っているか - daemonの
--node-ingress-ifaceが外部L2に面したNICを指しているか。入っていないならip linkでNIC名を確認して指定し直します - そのアドレスを他のホストが使っていないか。
arpingに複数のMACが返る場合はIPアドレスの重複です
- arpingは返るのにcurlが通らない
- 対象Podがdefault以外のSubnetに属しているか。default SubnetのPodはElasticIPの対象にできません
- Podが属するVpcのRouteTableに
type: internetGatewayのルートがあるか。無いと外部疎通が成立しません elasticipattachment.status.conditions[?(@.type=="Ready")].statusがTrueか
- Podを別のNodeへ移したあと通信が戻らない
kubectl get arpadvertisementのNODEが新しいNodeに変わっているか。変わっていればJuneau側の処理は終わっています- 変わっているのに戻らないなら、上流のneighborキャッシュが古いMACを保持しています。
ip neigh show 10.225.32.240で確認し、ip neigh flush 10.225.32.240で捨ててください。放置してもaging outすれば戻りますが、待ち時間は上流機器によります