API サーバーは Pod のアドレスを知らない
一言でいうと
Kubernetesは、Podにアドレスを直接与えません。APIサーバーはServiceのアドレスだけを管理し、Podのアドレスは、ノード上のネットワークプラグインが与えます。そのため、「Podが起動しない」と「Pod同士が届かない」は、別々の層の故障です。
なぜ必要なのか
コンテナを複数、1台のマシンに載せると、最初にぶつかるのがポートです。3つのチームがそれぞれ8080を使いたがれば、誰かが譲る必要があり、譲った側は設定でポートを変数にする必要があり、そうするとサービスディスカバリーもポートを知る必要があります。公式ドキュメントは、この道を「規模が大きくなると調整が非常に難しくなり、ユーザーが制御できないクラスターレベルの問題を生む」とまとめています。
Kubernetesは、その道を選びませんでした。代わりに、Podごとにアドレスを1つずつ与えます。Podの中では、コンテナ同士がlocalhostで会い、Podの外では、アドレスで会います。ポート調整という問題そのものがなくなります。
問題は、そのアドレスを、誰がどのように与えるかです。クラウドごとに違い、オンプレミスはまた違い、ある場所はルーティングで、ある場所はトンネルで解決します。Kubernetesは、この場所を空けておき、規格だけを決めました。それがCNI(Container Network Interface)です。
どう動くのか
公式ドキュメントの表現は明確です。「Kubernetesのネットワークモデルを実装するには、CNIプラグインが必要である」。そして、プラグインはCNI仕様v0.4.0以上と互換である必要があり、プロジェクトはv1.0.0互換を推奨しています。
誰が呼ぶのか。1.24より前は、kubeletがcni-bin-dirとnetwork-pluginオプションで、プラグインを管理していました。その2つのオプションは、1.24で削除されました。今、CNIを呼ぶのは、kubeletではなく、コンテナランタイム(containerd、CRI-O)です。そのため、「CNIの設定を変えたのに反映されない」という報告の半分は、kubeletを再起動した場合です。
どこを読むのか。設定は既定値が/etc/cni/net.d、バイナリは既定値が/opt/cni/binです。設定ファイル1つに、プラグインを複数書く形式がconflistで、前から順番に呼ばれます。
なぜ複数なのか。1つのプラグインがすべてを行うわけではないからです。公式ドキュメントが自分で挙げている例が2つあります。
| 機能 | 担当するプラグイン | 有効にする方法 |
|---|---|---|
hostPort |
portmap |
capabilitiesにportMappings: trueを置きます |
| 帯域制限 | bandwidth |
capabilitiesにbandwidth: trueを置き、Podにkubernetes.io/ingress-bandwidthアノテーションを付けます |
ループバックlo |
loopback |
ランタイムがサンドボックスごとに提供する必要があります |
ここから、運用上重要な事実が出てきます。hostPortを書いたのに、何も起きないことがあります。マニフェストはAPI検証を通り、PodはRunningになるのに、ポートは開きません。portmapがチェーンにないと、そうなります。静かに無視される種類の故障です。
アドレスはIPAMが与えます。bridgeのようなメインのプラグインの中のipam項目が、その仕事を担当します。最も一般的なhost-localは、名前のとおり、そのノードの中でだけアドレスを管理します。ノード同士が、お互いに何を配ったかを尋ねません。そのため、ノードごとに重ならない範囲を、あらかじめ切って渡す必要があり、その範囲がノードオブジェクトのspec.podCIDRです。公式の例が"subnet": "usePodCidr"と書かれているのは、そういう意味です。
3種類のアドレス。公式ドキュメントは、クラスターがPod・Service・ノードの3種類のアドレスを、重ならないように割り当てる必要があると固定し、それぞれを決める主体まで分けて書いています。Podはネットワークプラグイン、Serviceはkube-apiserver、ノードはkubeletまたはクラウドコントローラーです。3つの主体が互いに相談しないので、重なりを防ぐのは、設計者の担当です。
現場での姿
「ノード1台だけがPodを受け付けません」。Kubernetesは、この状況をテイントで表現します。公式ドキュメントの組み込みテイントの一覧に、node.kubernetes.io/network-unavailableが、「ノードのネットワークを使えない」という意味で入っています。そのため、報告は「PodがPending」として来ますが、直す場所はスケジューラーではなく、そのノードのプラグイン設定です。プラグインの設定ファイルがそのノードにだけない、バイナリのディレクトリが違う、ランタイムが別のパスを見ている、といった原因です。
「PodのIPが2つのノードで同じです」。host-localは、ノードの外を知らないので、2つのノードに同じ範囲を渡すと、平然と同じアドレスを配ります。症状はランダムな接続失敗として現れるので、原因を見つけるまでに時間がかかります。
「範囲を間違えたので、変更します」。ノードのspec.podCIDRは、一度値が入ると、別の値に変更できません。クラスター設計で、元に戻すことが最も難しい決定が、アドレスの範囲です。
公式ドキュメント: Network Plugins・Cluster Networking
次のラボですること
ノード3台に、/24ずつPodのアドレス範囲を固定し、その範囲で収められる数と、ノードが受け付けるPodの数のどちらが先に限界に達するかを計算します。次に、bridge・portmap・bandwidthの3つのプラグインをつなげたconflistを書き、hostPortと帯域のアノテーションを付けたPodを作って、どのプラグインがその仕事を担当するかを書きます。最後に、プラグインがないノードを模擬してNotReady条件を作ってみて、4通りのアドレス範囲の配置のうち、どれが重なるかを、計算で判定します。