Hybrid Cloud 上の Kubernetes 検証
この記事は,2022年3月1日から4月8日までの期間で参加した MIXI のインターン(Dive into MIXI GROUP 2021-2022 冬季)で行った内容をまとめたものです.
背景
Kubernetes のマネージドサービスである GKE(Google Kubernetes Engine) を利用するために必要な料金のうち,主要なものは以下の3つです.
このうちネットワークの料金については,データ転送量に比例して金額が上がっていきます. したがって,データ転送量が大きいサービスを GKE 上で動かす際には,ネットワークの料金が非常に高くなってしまいます.
リアルタイムでキャッシュが難しく,かつトラフィック容量が大きい WebRTC などのサービスを対象としてオンプレミスに逃すために,オンプレミス上で Kubernetes を構築することが求められていました.
CNI
CNI(Container Network Interface) プラグインとしては,cybozu-go/coil を採用しました.以下の2点が主な理由です.
- BGP スピーカが組み込まれていない
- 軽量で導入が容易
BGP スピーカが組み込まれていない CNI プラグインを採用したのは,MIXI が所有しているオンプレミス上の各サーバでは BGP スピーカが自動で立ち上がるように設定されていて,BGP スピーカがすでに組み込まれている Calico などを使用することが困難であったためです.
全体設計
全体設計は以下の図のとおりです.
LB や DNS を用意する手間を省くために,Control Plane Node は GCP 上で動かすように設計しています. また,オンプレミスのリソースが足りなくなった際に容易にスケールができるようにするために,Worker Node はオンプレミス上だけではなく GCP 上でも動かすように設計しています.
オンプレミス上の各サーバでは前述した BGP スピーカが立ち上がっており,BGP を用いて経路交換を行っています. それに加えて,オンプレミス上の Gateway と GCP 上の Cloud Router 間で BGP を用いて経路交換することにより,各サーバに割り当てられた pod-cidr の情報を交換しています.
ネットワーク要件
Kubernetes に求められる要件の1つとして,「クラスタ内の全ての Pod はクラスタ内の他の Pod と NAT なしで通信できなければならない」というものがあります. つまり,各 Pod に割り当てられた Internal IP を用いて通信できなければなりません.
今回の設計の場合,考えられる通信経路は以下の4つです.
それぞれについてどのようにパケットが送られるかを確認していきます.
考えやすいように,以下の図のように Internal IP, pod-cidr が割り当てられているとします.
オンプレミス→オンプレミス
Pod1(10.240.1.1) から Pod2(10.240.2.1) へパケットを送る場合を考えます.
- Pod1 は,Pod1 内の routing table を参照して,Node1 上にパケットを送ります
- Node1 は,Node1 内の routing table を参照して,オンプレミス上の Gateway にパケットを送ります
- オンプレミス上の Gateway は,Gateway の routing table を参照して,Node2 にパケットを送ります
- Node2 は,Node2 内の routing table を参照して,Pod2 にパケットを送ります
このように,各 Pod に割り当てられた Internal IP を用いて通信することができます.
ここで重要なことは,
- オンプレミス上の各サーバが行う BGP によって, オンプレミス上にある Node に割り当てられた pod-cidr とその Node の Internal IP の情報を Gateway が持っている
ということです.
GCP→オンプレミス
次に,Pod3(10.240.3.1) から Pod1(10.240.1.1) へパケットを送る場合を考えます.
- Pod3 は,Pod3 内の routing table を参照して,Node3 上にパケットを送ります
- Node3 は,Node3 内の routing table を参照して,VPC 内の Gateway にパケットを送ります
- VPC 内のGateway は, VPC 内の routing table(Routes) を参照して,オンプレミス上の Gateway に VPN を用いてパケットを送ります
- オンプレミス上の Gateway は,Gateway の routing table を参照して,Node1 にパケットを送ります
- Node1 は,Node1 内の routing table を参照して,Pod1 にパケットを送ります
このように,各 Pod に割り当てられた Internal IP を用いて通信することができます.
ここで重要なことは,
- Cloud Router を用いた BGP によって,オンプレミス上にある Node に割り当てられた pod-cidr とオンプレミス上にある Gateway の Internal IP の情報を Routes が持っている
- オンプレミス上の各サーバが行う BGP によって, オンプレミス上にある Node に割り当てられた pod-cidr とその Node の Internal IP の情報を Gateway が持っている
ということです.
オンプレミス→GCP
次に,Pod1(10.240.1.1) から Pod3(10.240.3.1) へパケットを送る場合を考えます.
- Pod1 は,Pod1 内の routing table を参照して,Node1 上にパケットを送ります
- Node1 は,Node1 内の routing table を参照して,オンプレミス上の Gateway にパケットを送ります
- オンプレミス上の Gateway は,Gateway の routing table を参照して,GCP 上の Cloud VPN に VPN を用いてパケットを送ります
- Cloud VPN は,VPC 内の routing table(Routes) を参照しますが,10.240.3.1 がどの Node にあるかが分からないため,パケットは Node3 に送られません
このように,各 Pod に割り当てられた Internal IP を用いて通信することができません.
ここで重要なことは,
- VPC 内で,どの pod-cidr がどの Node に割り当てられているかという情報が広告されていないため,適切な Node にパケットを送ることができない
ということです.
GCP→GCP
最後に,Pod3(10.240.3.1) から Pod4(10.240.4.1) へパケットを送る場合を考えます.
- Pod3 は,Pod3 内の routing table を参照して,Node3 上にパケットを送ります
- Node3 は,Node3 内の routing table を参照して,VPC 内の Gateway にパケットを送ります
- VPC 内のGateway は, VPC 内の routing table(Routes) を参照しますが,10.240.4.1 がどの Node にあるかが分からないため,パケットは Node4 に送られません
このように,各 Pod に割り当てられた Internal IP を用いて通信することができません.
ここで重要なことは「オンプレミス→GCP」の場合と同様に,
- VPC 内で,どの pod-cidr がどの Node に割り当てられているかという情報が広告されていないため,適切な Node にパケットを送ることができない
ということです.
Routes
以上のように,クラスタ内の全ての Pod がクラスタ内の他の Pod と NAT なしで通信できるようにするためには,どの pod-cidr がどの Node に割り当てられているかという情報を Routes が所持している必要があります.
Routes は,GCP のコンソール上や gcloud コマンドを用いて操作することができますが,手動で行おうとすると面倒であるだけではなく,ミスが発生しやすいです.
そこで,cybozu-go/coil が管理する pod-cidr の情報を監視して,Routes を管理するようなカスタムコントローラを作成することにしました.
カスタムコントローラ
カスタムコントローラの設計は以下の図のとおりです.
ここで,図に登場するカスタムリソースは以下のとおりです.
- AddressBlock
- cybozu-go/coil が管理するカスタムリソース
- Node に割り当てた pod-cidr の情報など
- Route
- 今回新しく定義したカスタムリソース
- Routes に作成する routing 情報など
実装したカスタムコントローラは route-controller と groute-controller の2つで,それらは route-controller-manager によって管理されているという構成になっています.
route-controller は cybozu-go/coil が管理する AddressBlock リソースを監視して,Route リソースを管理します.このコントローラは Route リソースを管理するだけで,Routes に対しては何も操作を行いません.
groute-controller は route-controller が管理する Route リソースを監視して,Routes を管理します.
リソースの削除
AddressBlock リソースが削除された場合,それに紐づく Route リソースや Routes は削除されるべきです.
今回の設計では,route-controller は AddressBlock リソースを監視しているため,AddressBlock リソースの削除を検知することができ,それに応じて Route リソースを削除することができます. また,groute-controller は Route リソースを監視しているため,Route リソースの削除を検知することができ,それに応じて Routes を削除することもできます.
しかし,削除のイベントが何らかの理由で取りこぼされてしまうと,削除されるべきものが削除されないまま残ってしまうといった問題があります.
そこで今回は,owner reference を用いて Route リソースの親リソースに AddressBlock を指定し,finalizer の機能を用いて Routes が削除されるまで Route リソースが削除されないように制御することで解決しました.
host network
groute-controller は Routes の作成・削除を行うために,Google Compute Engine API を叩く必要があります.
しかし,groute-controller が Routes の作成を行うときには,GCP 上の Pod への経路がまだ確立しておらず,API を叩くことができないといった問題があります
そこで今回は,groute-controller が host network 上で動くように設定することで解決しました.
reference
- https://cloud.google.com/kubernetes-engine/pricing
- https://cloud.google.com/vpc/docs/routes
- https://github.com/cybozu-go/coil
- https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
- https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/
- https://zoetrope.github.io/kubebuilder-training/