新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第9回です。前回まででクラスタの「載せ方」と「統制」を身につけました。本回から第3部「ネットワーク」に入ります。第1巻でも Service の 4 タイプ(ClusterIP/NodePort/LoadBalancer/ExternalName)には触れましたが、あれは kind 上の概念紹介が中心でした。本回はそれを運用者の視点で kube-proxy の仕組みまで踏み込んで掘り下げ、さらに第1巻では「概念だけ」だった LoadBalancer タイプを、MetalLB を使ってオンプレの kubeadm クラスタで実際に払い出します。第1巻では kind の extraPortMappings に頼って外部公開していましたが、本番の kubeadm オンプレには「外部 IP を自動で配る仕組み」がありません。その穴を埋めるのが MetalLB です。
操作は基本的に作業端末 k8s-ops(192.168.1.122)から kubectl で行います。プロンプトの $ は一般ユーザー developer を表します。第4回で配布した admin.conf(cluster-admin の全権)を使うので、本回の Service・MetalLB リソースの作成もこの権限で行えます。本回で作る YAML は k8s-ops の developer のホーム配下に作業ディレクトリを作ってまとめます(ファイルの所有者は developer・sudo は使いません)。
実行コマンド(k8s-ops・developer):
$ mkdir -p ~/networking && cd ~/networking
目次
今ここマップ(全 16 回中の現在地)
本シリーズは 6 部構成です。現在地は第3部「ネットワーク」の第9回です。
- 第1部 クラスタ構築(第1〜5回)
- 第2部 ワークロード管理(第6〜8回)
- 第3部 ネットワーク(第9〜11回)← 今ここ
- 第4部 ストレージ(第12回)
- 第5部 監視・運用(第13〜14回)
- 第6部 トラブルシュート(第15〜16回)
- Kubernetes v1.36.2
- kubectl v1.35.6(作業端末)
- MetalLB v0.16.1(Helm chart)
- AlmaLinux 10.2
- Service の API (
v1)と EndpointSlice の API(discovery.k8s.io/v1)はいずれも安定版で、マイナー番号が変わっても同じ - MetalLB のリソース API
metallb.io/v1beta1 - 確認日 2026-07-16
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- ClusterIP/NodePort/LoadBalancer/ExternalName の 4 タイプの動作原理と使い分けを説明できる
- kube-proxy(iptables/ipvs モード)が Service をどう実装しているかを理解する
- Service と EndpointSlice の対応を
kubectlで確認し、疎通トラブルの切り分けに使える - MetalLB を導入し、オンプレの
LoadBalancerService に外部 IP が払い出されることを実機で確認できる
Service の 4 タイプ(動作原理)
Service は「移り変わる Pod 群」への安定した宛先を提供する仕組みです。Pod は再作成のたびに IP が変わりますが、Service の名前と仮想 IP は変わりません。タイプは 4 つあり、「どこまで露出するか」で使い分けます。露出範囲は ClusterIP(内部だけ)⊂ NodePort(ノードのポート)⊂ LoadBalancer(外部の単一 IP)と入れ子になっており、ExternalName だけは毛色が違って Pod を持ちません。
- ClusterIP(既定):クラスタ内部でだけ使える仮想 IP を 1 つ割り当てる。外部からは直接触れない。Pod 間通信の基本形。
- NodePort:全ノードの高位ポート(既定
30000〜32767)で公開する。ノードIP:NodePortで外部から届く。内部的には ClusterIP も持つ。 - LoadBalancer:外部ロードバランサから単一の外部 IPで公開する。NodePort と ClusterIP を内包する。クラウドでは各社の LB が、オンプレでは MetalLB のような実装が IP を払い出す。
- ExternalName:外部のホスト名へ DNS の CNAME で委譲するだけ。Pod もセレクタも持たず、クラスタ内から外部サービスを名前で呼ぶときに使う。
実際に fanclub の Service を見てみます。第4回で移行した fanclub-api は 3 つの Service を持っています。
実行コマンド(k8s-ops・developer):
$ kubectl get svc -n fanclub
実行結果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
fanclub-backend ClusterIP 10.103.27.80 <none> 8080/TCP 3d
fanclub-db ClusterIP None <none> 5432/TCP 3d
fanclub-frontend ClusterIP 10.101.43.65 <none> 80/TCP 3d
3 つとも ClusterIP です。注目は fanclub-db の CLUSTER-IP が None になっている点です。これは ヘッドレス Service(spec.clusterIP: None)で、仮想 IP を持たず、DNS で各 Pod の IP を直接返します。StatefulSet の fanclub-db のように「個々の Pod を名前で区別したい」用途で使います。EXTERNAL-IP はすべて <none>――つまり現時点では外部から直接届くものはありません。ブラウザからの https://fanclub.local アクセスは第11回で Traefik を通して復活させます。

kube-proxy が Service を実装する
ここで大事な事実を押さえます。ClusterIP は「実体のない仮想 IP」です。その IP に紐づくネットワークインターフェースはどこにもありません。ではなぜ通信が届くのか――各ノードで動く kube-proxy が「その仮想 IP 宛のパケットを、背後の Pod のどれかへ転送する」ルールをカーネルに書き込んでいるからです。kube-proxy は DaemonSet として全ノードに 1 つずつ常駐しています。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n kube-system -l k8s-app=kube-proxy -o wide
実行結果(5 ノード分・抜粋):
NAME READY STATUS RESTARTS AGE IP NODE
kube-proxy-4xk9d 1/1 Running 0 3d 192.168.1.125 k8s-cp-01
kube-proxy-7mtqv 1/1 Running 0 3d 192.168.1.126 k8s-cp-02
kube-proxy-9wz2r 1/1 Running 0 3d 192.168.1.127 k8s-cp-03
kube-proxy-c8n5b 1/1 Running 0 3d 192.168.1.128 k8s-wl-01
kube-proxy-hp6lk 1/1 Running 0 3d 192.168.1.129 k8s-wl-02
kube-proxy には主に 2 つのモードがあります。動作モードは ConfigMap kube-proxy の mode フィールドで決まります。実機で確認します。
実行コマンド(k8s-ops・developer):
$ kubectl get configmap kube-proxy -n kube-system -o jsonpath='{.data.config\.conf}' | grep mode
実行結果:
mode: ""
mode が空文字("")になっています。これは「既定モードにお任せ」を意味し、Linux では iptables モードが選ばれます。kubeadm の初期状態はこれです。2 つのモードの違いは次のとおりです。
- iptables モード(既定):Service ごとに iptables のルールチェーンを作り、宛先を Pod へ DNAT する。ルール評価が線形なので、Service 数が数千規模になると遅くなる傾向がある。小〜中規模では十分。
- ipvs モード:Linux カーネルの IPVS(ハッシュテーブル方式)を使い、大量の Service でもルックアップが高速。負荷分散アルゴリズム(rr/lc など)も選べる。大規模クラスタ向け。
CKA では「ClusterIP は kube-proxy のルールがあって初めて機能する仮想 IP」「モードは iptables と ipvs があり、既定は iptables」という関係を押さえておけば十分です。モードの切り替えは ConfigMap の mode を ipvs にして kube-proxy を再起動しますが、本回では概念の理解にとどめます。
Service と EndpointSlice
Service は「宛先の看板」で、実際の転送先 Pod の一覧は別のリソース EndpointSlice が持ちます。Service の selector に一致した Pod の IP とポートが、コントローラーによって EndpointSlice に自動登録されます。kube-proxy はこの EndpointSlice を見て転送ルールを作ります。fanclub の EndpointSlice を見てみます。
実行コマンド(k8s-ops・developer):
$ kubectl get endpointslices -n fanclub
実行結果:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
fanclub-backend-x8k2v IPv4 8080 10.244.215.215 3d
fanclub-db-r7n4p IPv4 5432 10.244.119.159 3d
fanclub-frontend-q3m9t IPv4 80 10.244.215.217 3d
fanclub-backend の EndpointSlice には Pod IP 10.244.215.215:8080 が登録されています。これが「Service fanclub-backend:8080 宛の通信が最終的に届く先」です。ここでトラブルシュートの定石を覚えてください。Service に繋がらないとき、EndpointSlice の ENDPOINTS が空なら、selector と Pod のラベルが一致していないのが典型的な原因です(第10回・第16回のトラブルシュートで再登場します)。看板(Service)はあるのに転送先(EndpointSlice)が空、という状態を疑うわけです。
Service のフィールドを自分で調べたいときは kubectl explain が使えます。CKA 試験はドキュメント持ち込みですが、手元で素早く確認できるこの型は、身につけておくと試験でも実務でも役立ちます。
実行コマンド(k8s-ops・developer):
$ kubectl explain service.spec.type
実行結果(抜粋):
KIND: Service
VERSION: v1
FIELD: type <string>
ENUM:
ClusterIP
ExternalName
LoadBalancer
NodePort
DESCRIPTION:
type determines how the Service is exposed. Defaults to ClusterIP. Valid
options are ExternalName, ClusterIP, NodePort, and LoadBalancer.
...
オンプレの LoadBalancer 問題と MetalLB
ここで本回の核心です。クラウドで type: LoadBalancer の Service を作ると、クラウドプロバイダのコントローラー(CCM)が自動で LB を用意し外部 IP を払い出します。しかし私たちの kubeadm オンプレクラスタには CCM がありません。この状態で LoadBalancer Service を作るとどうなるかを、まず実機で確認します。テスト用の nginx を作ります。fanclub とは分けて、使い捨ての default 名前空間に作るので、以降の lb-test 関連コマンドには -n を付けません。
実行コマンド(k8s-ops・developer):
$ kubectl create deployment lb-test --image=nginx:1.27-alpine
$ kubectl expose deployment lb-test --type=LoadBalancer --port=80
実行コマンド(k8s-ops・developer):
$ kubectl get svc lb-test
実行結果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
lb-test LoadBalancer 10.107.55.219 <pending> 80:31820/TCP 15s
EXTERNAL-IP が <pending> のまま止まっています。IP を払い出す担い手がいないので、いつまで待っても付きません(NodePort 31820 は自動で割り当てられているので、ノード IP 経由なら届きます)。この穴を埋めるのが MetalLB です。MetalLB はオンプレの LAN 上の空き IP を LoadBalancer Service に払い出してくれます。
MetalLB には 2 つの動作モードがあります。L2 モードは 1 台のノードが ARP(IPv6 では NDP)で外部 IP の持ち主として応答する方式で、特別なネットワーク機器が不要なため小規模・検証環境に向きます。BGP モードはルーターと BGP を張って経路広告する方式で、複数ノードでの負荷分散に強い一方、対応ルーターが要ります。本シリーズは L2 モードを使います。
なお MetalLB の公式ドキュメントは metallb.universe.tf にあり、これは kubernetes.io の外です。CKA 試験では kubernetes.io 配下しか参照できないため、MetalLB そのものは試験の対象外です。ただし「オンプレで LoadBalancer をどう成立させるか」という考え方は運用で必須なので、本シリーズでは実機で体験しておきます。

MetalLB を Helm で導入する
MetalLB を Helm chart で導入します。バージョンは v0.16.1 にピン留めします。その前に alma-proxy の whitelist を 1 行足します。chart の配信元は metallb.github.io で、上で触れたドキュメントサイトの metallb.universe.tf とは別ホストです。whitelist にはどちらも入っていないので、chart を取りに行く側の 1 行を足します。これを忘れると、次の helm repo add が Forbidden で弾かれます。
実行コマンド(alma-proxy・root):
# echo 'metallb.github.io' >> /etc/squid/whitelist.txt
# systemctl reload squid
第2回で CDN 転送先を足したときと同じ手順です(/etc/squid/whitelist.txt は root 所有なので # プロンプト=root で編集し、squid は reload で反映されます)。作業端末 k8s-ops に戻って、Helm リポジトリを追加します。
実行コマンド(k8s-ops・developer):
$ helm repo add metallb https://metallb.github.io/metallb
$ helm repo update
実行結果:
"metallb" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "metallb" chart repository
Update Complete. ⎈Happy Helming!⎈
続いて metallb-system Namespace を作って install します。--version でチャートを固定します。なお v0.16 系の chart は既定で BGP 用のバックエンド frr-k8s(各ノードに常駐する DaemonSet)も同梱しますが、本回は L2 モードしか使わないので --set frrk8s.enabled=false で無効化し、構成を controller と speaker だけに絞ってシンプルにします。
実行コマンド(k8s-ops・developer):
$ helm install metallb metallb/metallb --namespace metallb-system --create-namespace --version 0.16.1 --set frrk8s.enabled=false
コントローラーとスピーカーが立ち上がるまで待ちます(Running になるまで数十秒かかります。まだ ContainerCreating なら少し置いて再実行してください)。controller は Deployment(IP 割り当ての頭脳)、speaker は DaemonSet(各ノードで ARP 応答を担当)です。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n metallb-system
実行結果:
NAME READY STATUS RESTARTS AGE
metallb-controller-6cf9c8d7b8-hs2xq 1/1 Running 0 65s
metallb-speaker-2vlq7 1/1 Running 0 65s
metallb-speaker-6mtn7 1/1 Running 0 65s
metallb-speaker-8kx4p 1/1 Running 0 65s
metallb-speaker-ptw9c 1/1 Running 0 65s
metallb-speaker-xr5nd 1/1 Running 0 65s
controller が 1 つ、speaker が 5 ノード分(全ノードに 1 つずつ)動いています。これで MetalLB 本体の準備は完了です。ただし、この時点ではまだ「どの IP 範囲を払い出してよいか」を教えていないので、先ほどの lb-test は依然 <pending> のままです。次でアドレスプールを設定します。
IPAddressPool と L2Advertisement を設定する
MetalLB に「払い出してよい LAN の空き IP 範囲」を IPAddressPool で教え、その範囲を L2 モードで広告する L2Advertisement を作ります。本シリーズの LAN(192.168.1.0/24)では 192.168.1.200〜192.168.1.210 を空きとして確保しています(DHCP レンジや 9 台の VM・HAProxy の .124 とは重複しません)。この範囲を 1 つのプールにします。マニフェストは、vi などのエディタで、冒頭で作った作業ディレクトリ ~/networking に metallb-pool.yaml というファイル名で次の内容を作成します(developer のホーム配下に作るのでファイルの所有者は developer・sudo は不要)。以降の kubectl apply はこのディレクトリにいる前提の相対パスで実行するので、別のシェルを開き直した場合は cd ~/networking で戻ってください。
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: lan-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.210
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: lan-l2
namespace: metallb-system
spec:
ipAddressPools:
- lan-pool
IPAddressPool が「配ってよい IP の在庫」、L2Advertisement が「その在庫を L2(ARP)で広告する」という宣言です。ipAddressPools でどのプールを広告するか紐付けます。適用します。
実行コマンド(k8s-ops・developer):
$ kubectl apply -f metallb-pool.yaml
実行結果:
ipaddresspool.metallb.io/lan-pool created
l2advertisement.metallb.io/lan-l2 created
プールを設定した瞬間、MetalLB は <pending> だった lb-test にプール先頭の IP を払い出します。もう一度 Service を見ます。
実行コマンド(k8s-ops・developer):
$ kubectl get svc lb-test
実行結果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
lb-test LoadBalancer 10.107.55.219 192.168.1.200 80:31820/TCP 5m
EXTERNAL-IP にプール先頭の 192.168.1.200 が付きました。<pending> からの変化が MetalLB の仕事です。LAN 上のクラスタノード以外のマシン(ここでは作業端末 k8s-ops)からこの IP へ HTTP アクセスしてみます。ノードではないマシンから単一 IP で届くことが、外部公開が成立している証拠になります。なお k8s-ops は第2回で設定したプロキシ環境変数の no_proxy に 192.168.1.0/24 を含んでいるため、この LAN 内の 192.168.1.200 へは alma-proxy を経由せず直接届きます(プールをあえて同じ /24 に置いているのはこのためです)。
実行コマンド(k8s-ops・developer):
$ curl -s http://192.168.1.200 | head -n 4
実行結果:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
クラスタ外の LAN アドレスから、Kubernetes 内部の nginx Pod に単一の外部 IP で届きました。これがオンプレ LoadBalancer の成立です。第11回では、この MetalLB が Traefik(Gateway API 実装)の LoadBalancer Service に 192.168.1.200 を払い出し、https://fanclub.local のブラウザアクセスが復活します。今回のテスト用 lb-test は役目を終えたので削除します(削除すると .200 はプールに戻り、第11回で Traefik が同じプール先頭の .200 を受け取ります)。
実行コマンド(k8s-ops・developer):
$ kubectl delete svc lb-test
$ kubectl delete deployment lb-test
MetalLB 本体(metallb-system)と IPAddressPool/L2Advertisement は残します。これらは第11回の HTTPS 復活に必要な恒久導入物です。消すのはテスト用の Service と Deployment だけです。
やってみよう
以下は本文で通した操作を自分の手で確かめ直すための演習です。本文のコマンドをすでに実行済みなら、テスト用の lb-test Service やアドレスプールが「残っている/削除済み」の状態に応じて、作り直しや削除で前提をそろえてから取り組んでください。
Service の 4 タイプを自分で確認する
kubectl explain service.spec.type と kubectl explain service.spec を使って、4 つのタイプ(ClusterIP/NodePort/LoadBalancer/ExternalName)と clusterIP/ports/selector フィールドの説明を自分で読んでください。あわせて kubectl get svc -n fanclub で fanclub-db の CLUSTER-IP が None(ヘッドレス)であることを確認してください。
pending から外部 IP への変化を体験する
テスト用の type: LoadBalancer Service を作り、EXTERNAL-IP が <pending> のままになることを確認してください(本文で既に metallb-pool.yaml を適用済みだと即座に IP が付いてしまうので、その場合はいったん kubectl delete -f metallb-pool.yaml でプールを外すと <pending> を再現できます)。そのうえで metallb-pool.yaml を適用し、同じ Service に 192.168.1.200 台の IP が付くことを対比して観察してください(確認後、外したプールは第11回で使うので必ず再適用しておきます)。
外部 IP への疎通と EndpointSlice の確認
払い出された EXTERNAL-IP へ LAN 上のクラスタノード以外のマシン(作業端末 k8s-ops でかまいません)から curl して nginx の応答が返ることを確認してください。あわせて kubectl get endpointslices(テスト用 Service は default 名前空間なので -n は不要)で、その Service の背後にどの Pod IP が登録されているかを確認してください。確認できたらテスト用の Service と Deployment を削除します(MetalLB 本体とプールは残す)。
まとめ
本回では Service の仕組みとオンプレの外部公開を掘り下げました。要点は、①Service の 4 タイプは ClusterIP ⊂ NodePort ⊂ LoadBalancer と露出範囲が入れ子で、ExternalName だけは Pod を持たない別枠、②ClusterIP は実体のない仮想 IP で、各ノードの kube-proxy(既定は iptables モード)が転送ルールを書いて初めて機能する、③実際の転送先 Pod は EndpointSlice が持ち、ここが空なら selector 不一致を疑う、④オンプレ kubeadm には CCM が無いため LoadBalancer は <pending> になるが、MetalLB(L2 モード)を入れて IPAddressPool を設定すれば LAN の空き IP を払い出せる、でした。導入した MetalLB は第11回の HTTPS 復活の土台になります。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- kubeadm のオンプレクラスタは、標準で
LoadBalancerService に外部 IP を払い出す。 - ClusterIP は外部から直接アクセスできる仮想 IP である。
- NodePort の既定のポート範囲は 30000〜32767 である。
- ClusterIP の仮想 IP は、各ノードの kube-proxy が転送ルールを書くことで初めて機能する。
- kube-proxy の既定モードは ipvs であり、iptables モードは大規模向けの選択肢である。
- Service の selector に一致する Pod が無いと、EndpointSlice の ENDPOINTS は空になり疎通しない。
- ExternalName タイプの Service は Pod を持たず、外部ホスト名へ DNS の CNAME で委譲する。
- MetalLB の L2 モードは、1 台のノードが ARP で外部 IP の持ち主として応答する。
- ヘッドレス Service は
clusterIP: Noneを指定し、仮想 IP を持たず個々の Pod の IP を返す。
解答と解説
1=×(CCM が無いので払い出せない。MetalLB 等が必要)/2=×(内部専用。外部からは直接触れない)/3=○(既定 30000-32767)/4=○(kube-proxy のルールで機能する仮想 IP)/5=×(既定は iptables。ipvs が大規模向け)/6=○(ENDPOINTS 空=selector 不一致の典型)/7=○(Pod を持たず CNAME 委譲)/8=○(L2 は 1 ノードが ARP 応答・フェイルオーバー可)/9=○(clusterIP: None のヘッドレス)
次回予告
次回・第10回は、通信を「繋ぐ」から「絞る」へ視点を移します。NetworkPolicy で Pod 間通信をホワイトリスト方式で制御する方法を、Calico と Cilium の比較を交えて学び、あわせて疎通トラブルのミニ切り分け演習を行います。
