コンテンツにスキップ

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

詳細はVpc / Subnetを参照してください。

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: AvailableADDRESSが埋まればアドレス確保完了です。この時点ではまだどの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をNATGatewayServiceLoadBalancerからも参照できます。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だけになります。

うまくいかないとき

  1. ElasticIPのADDRESSが埋まらない
    • PHASE: Errorなら、AddressPoolのadvertiseModeとExternalNetworkのtypeが食い違っています。どちらもarpに揃えてください
    • PHASE: Pendingのままなら、AddressPoolの範囲を使い切っている可能性があります。kubectl get allocationleaseで払い出し済みのアドレスを確認できます
  2. kubectl get arpadvertisementにエントリが無い
    • ElasticIPの場合、ElasticIPAttachmentがPHASE: Attachedになっているか。外れている間は意図的に削除されます
    • ServiceLoadBalancerの場合、status.advertisingNodesが空でないか。Local backendが1つも無ければ応答するNodeも決まりません
  3. arpingが返ってこない
    • クライアントがNodeと同じL2サブネットにいるか。ルータを挟んでいると届きません
    • 応答するはずのNodeでkubectl juneau bpf dump external_arp_tableを実行し、期待するアドレスが入っているか
    • daemonの--node-ingress-ifaceが外部L2に面したNICを指しているか。入っていないならip linkでNIC名を確認して指定し直します
    • そのアドレスを他のホストが使っていないか。arpingに複数のMACが返る場合はIPアドレスの重複です
  4. arpingは返るのにcurlが通らない
    • 対象Podがdefault以外のSubnetに属しているか。default SubnetのPodはElasticIPの対象にできません
    • Podが属するVpcのRouteTableにtype: internetGatewayのルートがあるか。無いと外部疎通が成立しません
    • elasticipattachment.status.conditions[?(@.type=="Ready")].statusTrue
  5. Podを別のNodeへ移したあと通信が戻らない
    • kubectl get arpadvertisementNODEが新しいNodeに変わっているか。変わっていればJuneau側の処理は終わっています
    • 変わっているのに戻らないなら、上流のneighborキャッシュが古いMACを保持しています。ip neigh show 10.225.32.240で確認し、ip neigh flush 10.225.32.240で捨ててください。放置してもaging outすれば戻りますが、待ち時間は上流機器によります

参照