クラスタを増やす前に終えるべき設計
一言でいうと
ClusterMeshは、複数のクラスターのアイデンティティとServiceを同期して、単一クラスターと同じポリシーモデルを維持します。ただし、CIDRとCAの設計は、あとから変更するのがほぼ不可能なので、最初に確定させておく必要があります。
なぜ必要なのか
クラスターが2つ以上になると、すぐに疑問が生まれます。リージョン障害のときに別のクラスターへ切り替えられるのか、クラスター間の呼び出しに同じセキュリティポリシーを適用できるのか。サービスメッシュで解くこともできますが、そうするとプロキシ層がもう1つ載ることになります。
ClusterMeshのアプローチは違います。データプレーンに追加のホップを作らずに、コントロールプレーンだけをつなぎます。各クラスターがclustermesh-apiserverで自分のService・アイデンティティ・エンドポイントを公開し、相手のクラスターのagentがそれをwatchします。同期が終わると、パケットは既存のルーティングモードで直接流れます。中央のゲートウェイも、単一障害点もありません。
どう動くのか
前提条件が、そのまま設計上の決定です。
- クラスターごとに一意の名前とID(1–255)が必要です。IDの幅が8ビットなので、最大255個のクラスターまで束ねることができます。
- PodCIDRとノードIPの範囲が重複してはいけません。 重複すると、同じアドレスが2つのクラスターで別のPodを指すことになり、ipcacheのマッピングが成立しません。最もよくある設計ミスで、運用中に直すには、事実上の再構築です。
- すべてのクラスターが同じCAを共有する必要があります。 クラスター間の通信もmTLSで相互認証しますが、信頼の起点が異なると、相手の証明書を検証できません。そのため、2つ目のクラスターをつなぐ前に、最初のクラスターのCAをコピーして埋め込みます。
同期されるのは、Service(グローバルとして指定されたもの)、アイデンティティ、そしてipcacheです。アイデンティティが境界を越えて意味を保つことが核心です。クラスターBのapp=backendのPodのアイデンティティが、クラスターAのipcacheにも伝播されるので、マルチクラスターのポリシーが、単一クラスターとまったく同じラベルの文法で動作します。
グローバルServiceは、アノテーションで作成します。
| アノテーション | 値 | 動作 |
|---|---|---|
| service.cilium.io/global | "true" | クラスター間のバックエンドを合算 |
| service.cilium.io/affinity | local | ローカルのバックエンドを優先し、すべてダウンしたらリモート |
| service.cilium.io/affinity | remote | リモートを優先(ドレイン・カナリア) |
| service.cilium.io/affinity | none | 全クラスターに均等に分散 |
実務の標準はaffinity: localです。平常時はクラスター内で処理してレイテンシを節約し、ローカルのバックエンドがすべてunhealthyになると、リモートに切り替わります。ところが、ここには隠れた依存関係があります。「unhealthyの判定」はreadinessベースです。 readinessProbeがずさんだと(たとえば、プロセスが生きてさえいればReady)、事実上故障したバックエンドがReadyのまま残り続け、フェイルオーバーが起きません。ずさんなreadinessProbeは、ずさんなフェイルオーバーになります。
ポリシー側の落とし穴も1つあります。ClusterMesh環境で、ラベルセレクターからio.cilium.k8s.policy.clusterを抜くと、両方のクラスターの同じラベルのPodがすべてマッチします。 「DBは自分のクラスターのアプリからだけアクセスできる」ことを意図したのに、リモートクラスターの同名のワークロードまで開いてしまう事故が、ここで起きます。
運用ルールもいくつかあります。接続されたクラスター間のCiliumのバージョンスキューは、マイナーバージョン1つ分までしかサポートされないので、「1クラスターずつアップグレードし、すべてが同じバージョンになる前には、次のマイナーに進まない」が原則です。クラスターを削除するときは、グローバルServiceへの依存から整理する必要があります。affinity: noneでリモートに依存していたServiceがあると、disconnectの瞬間にバックエンドが減ります。そして、clustermesh-apiserverが落ちても、すでに同期されたServiceとアイデンティティで、トラフィックは流れ続けます。 止まるのは、新しい変更の伝播です。
外部ワークロード(External Workloads)の機能は、VMやベアメタルにagentをインストールして、クラスターに参加させるものです。そのワークロードもアイデンティティを受け取るので、同じラベルベースのポリシーが適用されます。
現場での姿
筆者のホームラボはまだ単一クラスターですが、3ノードから7ノードに拡張する中で、まったく同じ種類の教訓を得ました。
コントロールプレーンを3台に増やして、etcdメンバーを3つ確保したのでHAになったと思っていましたが、違いました。kubeadm-configのcontrolPlaneEndpointが、VIPやDNSではなく、cp-1の物理IP10.0.0.120であり、そのアドレスが、7つのノードのkubelet設定とすべてのkubeconfig、そしてapiserver証明書のSANに埋め込まれていたのです。cp-1が落ちると、etcdのクォーラムは2/3で問題なく、残りのapiserverプロセスも正常なのに、誰も入口を見つけられない状態になります。
この事例は、そのままClusterMeshの設計に当てはまります。コントロールプレーンのデータの可用性とアクセスの可用性は、別の問題であり、エンドポイント・CIDR・CAのような値は、クラスターを作成したあとで変更するのが非常に苦痛な、数少ない設定です。そのため、クラスターを1つしか使わない予定であっても、PodCIDRを重複しないように設定し、CAの設計をあらかじめ済ませておくと、将来の自分を救うことになります。同じホームラボで、WireGuardの暗号化はPeers: 2、ポート51871でノード間のトンネルが確立されていますが、この転送暗号化の層は、クラスターをまたいでも同じ方式で拡張されます。
次のクイズで確認すること
ClusterMeshは、クラスターが2つ以上ないとラボで実習できないため、このモジュールは概念とクイズで締めくくります。クラスターIDの範囲、CIDRとCAの前提、グローバルServiceのアフィニティとreadinessへの依存、そしてポリシーでクラスターラベルを抜かしたときの落とし穴を点検してください。