ServiceはIPではなく「名前」を売る商品だ
一言でいうと
Pod IPは再起動のたびに変わる揮発性のアドレスです。Serviceはその上に変わらない名前と仮想IPを載せ、ラベルセレクターで背後に付くPodを更新し続けます。そのため、Serviceの本質はロードバランシングではなく、間接アドレス(indirection)です。
なぜ必要なのか
Podはいつでも死んで、また起動します。起動するたびにIPが変わります。フロントエンドがバックエンドのPodのIPを直接知っていると、バックエンドが1回再起動するたびにフロントエンドを修正しなければなりません。スケールアウトするとIPが複数になり、そのリストを誰が管理するのかも問題です。
Serviceはこの問題を「名前という1層」で解決します。クライアントはbackendという名前だけを知っていて、その名前が実際にどのIPに解決されるかはKubernetesが管理します。名前はDNSで解決されます。同じネームスペースならbackend、別のネームスペースならbackend.other-ns、完全な形はbackend.other-ns.svc.cluster.localです。
間接アドレスの価値は、それがないときに最も痛く表れます。ホームラボのクラスターを3ノードから7ノードに拡張してコントロールプレーンを3台にしたとき、kubeadm-configを開いてみると、こうなっていました。
controlPlaneEndpoint: 10.0.0.120:6443 # ← cp-1 의 물리 IP
VIPでもDNSでもない最初のノードの実際のIPが埋め込まれていました。そのため、cp-1が停止しても、etcdのクォーラムは2/3で無事で、残りのapiserverプロセスも生きているのに、ワーカー7台のkubeletとすべてのkubeconfigがそのアドレスしか知らないので、誰も入口を見つけられない状態になりました。さらに、apiserverの証明書のSANに他のコントロールプレーンのIPがなく、直接接続してもTLS検証が失敗しました。ServiceがPodに対してしてくれることは、まさにこれです。背後で何が変わっても、クライアントが知っている名前をそのまま保つことです。
どう動くのか
Serviceのspecで最も間違えやすいのが、3つのポートです。
| フィールド | 誰が見るか | 値の意味 |
|---|---|---|
port |
クライアント | ServiceのClusterIPで開くポート |
targetPort |
Pod | 実際のコンテナが待ち受けるポート(数値またはポート名) |
nodePort |
クラスターの外 | すべてのノードで開くポート(30000–32767) |
targetPortにコンテナのポートの名前を書くと(targetPort: api)、コンテナごとに実際のポート番号が違っても、Serviceを修正しなくて済みます。これがnamed portの要点です。
Serviceのタイプは、上へ積み重なります。ClusterIP(クラスター内部のみ) → NodePort(ClusterIP + ノードのポート) → LoadBalancer(NodePort + 外部LB)。ここに、セレクターのない特殊な形が2つあります。ExternalNameはDNSのCNAMEだけを作り、ヘッドレス(clusterIP: None)は仮想IPを作らず、DNSがPodのIPをそのまま返します。
ヘッドレスはStatefulSetとペアです。StatefulSetのserviceNameにヘッドレスServiceを指定すると、各Podが固有のDNS名を持ちます(次のコードブロックのプレースホルダーはPod名、Service名、ネームスペースです)。
<파드이름>.<서비스이름>.<네임스페이스>.svc.cluster.local
cache-0.cache-hs.ckad-net.svc.cluster.local
DBのレプリケーションのように「インデックス0がprimary」といった役割があるワークロードは、ロードバランシングではなく個別のアドレスが必要なので、この組み合わせを使います。
IngressはL7です。1つの入口から、ホストとパスで振り分けて複数のServiceに送ります。pathTypeが重要で、Prefixはパスセグメント単位の前方一致(/apiが/api/usersも捕捉する)、Exactは完全一致です。Ingressオブジェクトだけでは何も起きず、Ingressコントローラーがあって初めて実際のルーティングが行われます。
NetworkPolicyはデフォルトが許可です。ポリシーが1つもなければ、すべてのPodが互いに通信します。ところが、あるPodにポリシーが1つでも適用された瞬間、そのPodは許可リストモードになり、明示されたものだけが許可されます。policyTypesにIngressだけを書くと、出ていくトラフィックは制御されません。そして、ポリシーは加算されるだけです。複数のポリシーが適用されると、和集合が許可されます。
現場での姿
ホームラボのクラスターをkubeadm 1.34 + Cilium 1.20で作り直す際に、kube-proxyをそもそもインストールしませんでした(--skip-phases=addon/kube-proxy)。それでもServiceが動作するかを、実測で確認しました。
kube-proxy 파드 수: 0
노드의 iptables KUBE- 체인 개수: 0
kube-proxyを使うクラスターなら、KUBE-SERVICES、KUBE-SVC-*、KUBE-SEP-*のチェーンがServiceの数に比例して数十から数百個あります。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)
ここで学ぶべきことは、Serviceという抽象はそのままなのに、実装はまったく入れ替えられるという点です。マニフェストは1文字も変わらず、PodからService名で接続してHTTP 200を受け取る検証も、そのまま通りました。
NetworkPolicy側でも実測がありました。CiliumのL7ポリシーで、同じIP・同じポートなのに、HTTPメソッドでトラフィックを振り分けました。
정책 적용 전: GET / → 200, POST / → 200
정책 적용 후: GET / → 200, POST / → 403
標準のNetworkPolicyはL3/L4(Podセレクター、ポート)までしか表現できず、メソッドやパス単位はCNIの拡張(CiliumNetworkPolicy)やサービスメッシュの領域です。CKADの範囲は標準のNetworkPolicyですが、どこまでが標準でどこからが拡張かは、知っておいたほうがよいでしょう。
最後にセレクターの事故を1つ。Serviceのセレクターが、Podのラベルと1文字でも違うと、エンドポイントが空のまま作られます。エラーも出ず、イベントもありません。kubectl get endpoints <이름>(プレースホルダーは名前です)が<none>なら、十中八九セレクターの打ち間違いです。
次のラボですること
ckad-netネームスペースでClusterIP・NodePortを作成し、port/targetPort/nodePortを区別して指定します。named portでtargetPortを接続し、ヘッドレスService + StatefulSetでPodごとのDNS名の規則を確認し、Ingressでホスト・パスのルーティングを書き、NetworkPolicyでフロントエンドからバックエンドへのみ許可する許可リストを作成します。この環境には実際のCNIデータプレーンがないため、ポリシーが本当にパケットを遮断するかは確認できません。オブジェクトを正確に書くことが目標で、動作原理はクイズで確認します。