Serviceはプロキシではなく約束だ
一言でいうと
Serviceオブジェクトは、トラフィックを運びません。セレクターでバックエンドの一覧(EndpointSlice)を作っておく宣言にすぎず、実際にパケットを回すのは、ノードのデータプレーン(kube-proxyのiptables/nftables、またはCNIのeBPF)です。この分離を知っていれば、「Serviceはあるのに接続できない」を3つの層に分けて見られます。
なぜ必要なのか
Podは死んでは再び起動し、そのたびにIPが変わります。そのため、名前と安定した仮想IPが必要でした。Serviceは、この2つを提供します。
問題は、誰がバックエンドの一覧を維持するのかです。Serviceにセレクターを書くと、エンドポイントコントローラーが、そのラベルに合っていてReadyのPodを見つけて、EndpointSliceを埋めます。セレクターが1文字でもずれると、EndpointSliceは空のまま正常に作成されます。エラーは出ません。Serviceも、Deploymentも、Podも、すべてReadyなのに、接続だけができません。
そのため、サービス障害の診断の最初の質問は、常に同じです。「エンドポイントにIPがあるか」です。あればデータプレーンの問題で、なければセレクターかPodのReadyの問題です。
どう動くのか
| タイプ | 行うこと | 使う場面 |
|---|---|---|
| ClusterIP | クラスター内部の仮想IP1つ | デフォルト |
| NodePort | すべてのノードの30000–32767ポートを開きます | 外部からの一時的なアクセス |
| LoadBalancer | NodePort + 外部LBのプロビジョニング | クラウド / MetalLB |
| ExternalName | IPなしでCNAMEだけを返します | クラスター外のシステムを指す |
| headless (clusterIP None) | VIPなしでPodのIPを直接返します | StatefulSet、クライアント側LB |
ポートが2つ以上なら、各ポートにnameが必須です。Ingressやほかのオブジェクトが、ポートを名前で参照できるようにするためです。
NetworkPolicyで最もよく間違える箇所は、from配列の構造です。
fromの項目が2つで、それぞれnamespaceSelector、podSelectorならORです。どちらか一方だけ合えば許可されます。fromの項目が1つで、その中に両方があればANDです。そのネームスペースの、そのラベルのPodだけが許可されます。
YAMLではダッシュ(-)1つの違いですが、意味は正反対です。試験でも実務でも、ここで分かれます。
現場での姿
事例1: kube-proxyがそもそもないクラスター。ホームラボをkubeadm 1.34で再構築する際に、--skip-phases=addon/kube-proxyでkube-proxyを最初からインストールせず、Cilium 1.20.1をkubeProxyReplacement=trueで導入しました。主張だけでは足りないので、実際に数えてみました。
kube-proxy 파드 수: 0
노드의 iptables KUBE- 체인 개수: 0
kube-proxyを使うクラスターなら、KUBE-SERVICES、KUBE-SVC-*、KUBE-SEP-*チェーンが、サービス数に比例して数十から数百個存在します。0個でした。では、サービスはどうやって動いているのでしょうか。eBPFマップをダンプすると、次のように出てきます。
SERVICE ADDRESS BACKEND ADDRESS (REVNAT_ID) (SLOT)
10.96.0.10:53/TCP (1) 10.244.0.150:53/TCP (4) (1)
10.96.0.1:443/TCP (1) 10.0.0.120:6443/TCP (1) (1)
カーネル内のハッシュマップに、フロントエンドとバックエンドが直接入っています。iptables方式は、サービスが増えるほどルールのチェーンが線形に長くなりますが、eBPFはハッシュマップの参照なので、サービス数とは無関係に一定です。Serviceオブジェクトはそのままで、データプレーンだけを入れ替えたわけで、この分離が実物で確認できる瞬間です。
ここに、ブートストラップの問題が1つ付いてきます。kubeProxyReplacementを有効にするときは、k8sServiceHost=10.0.0.120、k8sServicePort=6443を一緒に渡す必要があります。kube-proxyがないと、kubernetesサービスのClusterIP(10.96.0.1)を実際のapiserverのアドレスに変えてくれる主体がいませんが、その役割を担うCilium自身が、まだ起動する前だからです。鶏と卵なので、実際のアドレスを直接教える必要があります。
事例2: L7はiptablesではできません。同じクラスターで、GETは200、POSTは403に分けるポリシーを設定してみました。同じIP、同じポートなのに、HTTPメソッドで分かれます。iptablesはL3/L4なので、メソッドが見えず、原理的に不可能です。標準のNetworkPolicyも同様に、L3/L4までです。CKAの範囲のNetworkPolicyが、どこまでできるのか線を引くのに、よい例です。
事例3: Endpointsはもう過去のものです。v1.33でEndpoints APIがdeprecatedになりました。コードやスクリプトがEndpointsを直接読んでいるなら、kubernetes.io/service-nameラベルでEndpointSliceを照会するように変える必要があります。アップグレード前の点検項目の定番です。
次のラボですること
最初のラボで、ClusterIP、NodePort、headless、multi-port、ExternalName、Ingressを作成し、セレクターのタイプミスでエンドポイントが空になる状況を自分で作って直します。2つ目のラボでは、NetworkPolicyをデフォルト拒否から始めて、6つの形に絞り込んでいきます。このラボ環境には、ポリシーを強制するCNIがないため、実際のトラフィックの遮断は確認できず、オブジェクトのスペックだけを採点します。その代わり、ANDとORの違いのような、スペック上の落とし穴を集中的に扱います。