この記事は2024年後半頃の構築作業をもとにしています。当時のバージョンは Calico v3.28.1 / MetalLB v0.14.8。手順や推奨構成はその後変わっている部分があるので、最新の公式ドキュメントと併せて読んでください。
経緯
オンプレの物理サーバ(Hyper-V上のVM)でK8sクラスタを組んだ。クラウドのマネージドK8sなら type: LoadBalancer のServiceを作れば、クラウド側の連携コンポーネントが勝手に外部IP(AWSの場合はELBのDNS名)を払い出してくれる。しかしオンプレだと当然そんなものはなく、EXTERNAL-IPが <pending> のまま永遠に待ちぼうけになる。
要件は次の2つ。
- NetworkPolicyでアクセス制御をしたい
- LoadBalancerタイプのServiceを使いたい(クラスタ外からのアクセスを各ノードに分散したい)
この2つを満たす構成として、CNIにCalico、LoadBalancer実装としてMetalLBをBGPモードで採用した。
構成の全体像
- Calico (CNI。クラスタ内のL3ネットワーク + NetworkPolicy担当)
- MetalLB (BGPモードで、LoadBalancer ServiceのIPを外部のBGPルータ2台に経路広報する)
- 外部のBGPルータがLBのVIP宛のトラフィックを受け、各ノードに振り分ける
なお当時使ったのはMetalLBの「native mode」(組み込みのBGP実装)だが、現在の公式ドキュメントではFRR-K8sモードが推奨になっている。これから入れるならそちらを検討した方がよさそう。
ハマりどころ・気づき
1. Calicoは最初からBGPを喋っている
当初は「MetalLBがBGPなんだからCalicoもBGPモードにするんでしょ」と思ってCalicoのBGP化を進めていた。ところが kubectl calico node status を叩いてみると、BGP設定を何もデプロイしていない時点で、全ノード間のBGPネイバー(node-to-node mesh)がEstablishedになっていた。
IPv4 BGP status
+--------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+------------+-------------+
| 10.x.x.x | node-to-node mesh | up | 2024-08-01 | Established |
| ... | node-to-node mesh | up | ... | Established |
+--------------+-------------------+-------+------------+-------------+
Calicoはデフォルトでノード間フルメッシュのBGPを組み、Podへの経路を交換している(今回の構成ではPod間トラフィック自体はIPIPでカプセル化される設定 CALICO_IPV4POOL_IPIP: Always のまま)。「BGPモードにする」という作業の実態は、外部ルータとのピアリングや経路広報などのBGPConfigurationを追加することであって、BGP自体は最初から動いている。
2. BGPConfigurationは kubectl apply できない
Calicoの projectcalico.org/v3 APIグループのリソース(BGPConfigurationなど)は、素の kubectl apply だとデプロイに失敗する。calicoctl(kubectlプラグインとして kubectl calico で使える)が必要。calicoctlがこのAPIグループのバリデーションとデフォルト値補完を担っているため。
# kubectl calico apply -f bgpconfig.yaml
Successfully applied 1 'BGPConfiguration' resource(s)
kubectlだけで完結させたいならCalico APIサーバーを別途導入する必要がある。地味に引っかかるポイント。
現在のCalicoにはnative v3 CRDsという仕組みがあり、APIサーバーやcalicoctlなしで kubectl から直接 projectcalico.org/v3 リソースを管理できる選択肢が増えている。当時はcalicoctl一択だった。
3. で、CalicoのBGP設定は結局解除した
BGPConfigurationを作り、外部ルータとのピアリング設定を検討していたところで、「そもそもCalico側で外部ルータとBGPを喋る必要はないのでは?」と気づいた。外部にLBのIPを広報するのはMetalLBのspeakerの仕事で、Pod間の通信はCalicoのデフォルト構成のままで既に動いている。結局、追加したBGPConfigurationは削除し、Calicoはデフォルト設定のままとした。
役割分担を整理するとこうなる。
- Calico: クラスタ内(Pod間)の経路。ノード間のBGPメッシュはデフォルトのまま
- MetalLB: クラスタ外向け。LoadBalancer ServiceのIPを外部BGPルータに広報
なお、CalicoとMetalLBを併用する場合の制約(同じルータに両方がBGPピアを張ると衝突する問題)がMetalLBのドキュメントに書かれている。今回はCalico側で外部ピアを張らない構成にしたので、この制約自体を考慮不要にできた、という面もある。ちなみにASはCalicoのメッシュ(デフォルトの64512)とMetalLB(プライベートASを別途採番)で別になっている。
4. kube-proxyのstrictARP
MetalLB導入の前提として、kube-proxyをIPVSモードで使っている場合は strictARP: true にする必要がある(Kubernetes v1.14.2以降。公式のInstallation > Preparationに記載)。
kubectl edit configmap -n kube-system kube-proxy
ipvs:
strictARP: true
MetalLBの設定
マニフェスト(metallb-native.yaml)でcontrollerとspeakerを入れたあと、設定系リソースをデプロイする。実際に使ったのは次の3種類。
IPAddressPool: LBとして払い出すIPアドレス範囲。
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: lb-pool
namespace: metallb-system
spec:
addresses:
- 10.x.x.1-10.x.x.254
BGPPeer: 外部BGPルータとのピア設定。ルータ2台分作って冗長化。
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router1
namespace: metallb-system
spec:
myASN: 65xxx
peerASN: 65xxx
peerAddress: 10.x.x.x
BGPAdvertisement: プールのIPをピアに広報する設定。specを書かなければ全プールがデフォルト設定で広報される。
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: advertisement1
namespace: metallb-system
これで type: LoadBalancer のServiceを作るとプールからIPが払い出され、外部ルータ経由でクラスタ外から届くようになった。
まとめ
- オンプレでLoadBalancer Serviceを使うならMetalLB。ルータと連携できる環境ならBGPモードが素直
- 「MetalLBがBGPだからCNIもBGPモードに」は早合点だった。外部への経路広報はMetalLBの仕事で、Calicoはデフォルトのnode-to-node meshのままでよい
- Calicoの
projectcalico.org/v3リソースは(当時は)calicoctl必須 - kube-proxyがIPVSモードならstrictARPを忘れずに