Certified Kubernetes Administrator (CKA) #18 Networking 1: Service (ClusterIP/NodePort/LoadBalancer/ExternalName)

#17 Storage 2 でストレージドメインを終えました。ここから CKA の出題比重 20% を占める Services and Networking ドメインに入ります。その最初の記事である今回は、ネットワーキングの土台である Service を扱います。

Pod はいつでも死んで再び立ち上がり、そのたびに IP が変わります。こうして揺れる Pod IP に直接トラフィックを送ることはできません。Service は揺れる Pod の集合の前に安定した仮想 IP と DNS 名を立ててくれる抽象化 です。このトピックに K8s 基礎 #5 で概念として初めて出会ったなら、今回の記事は同じ主題を CKA 運用者の視点で見直します。Service が どのように selector で Pod を選び、Endpoints を作り、kube-proxy がそれをノードのルールとして実装するのか、そして Service が動かないときにその経路のどこが切れたかを追う方法を整理します。

Service が解く問題 #

Deployment で立てた Pod が 3 個あるとします。この 3 個はローリングアップデート、ノード障害、スケール調整のたびに死んで再び立ち上がり、そのたびに新しい IP を受け取ります。クライアントが特定の PodIP を覚えておく方式はすぐに壊れます。

Service はこの問題を 3 つの方法で解きます。

  • 安定した仮想 IP (ClusterIP)。Service オブジェクトが生きている間は IP が変わりません。
  • DNS 名。CoreDNS が サービス名.ネームスペース.svc.cluster.local を ClusterIP に解決してくれます。
  • 負荷分散。selector にかかった複数の Pod へトラフィックを分散します。

核心は Service が Pod を直接指さない という点です。Service は label selector という条件を宣言するだけで、その条件に合う Pod の実際の IP 一覧は Endpoints (または EndpointSlice) という別オブジェクトに埋められます。この分離が運用とデバッグの出発点です。

selector → Endpoints → kube-proxy #

Service がトラフィックを運ぶまでの経路は 3 段階です。CKA で「Service が動かない」という問題は、ほぼ常にこの 3 つのうち 1 つが切れたものです。

段階主体やること
1. 選択Service の selectorどの label を持つ Pod を対象にするかを宣言
2. 充填endpoints controllerselector に合い Ready な PodIP を Endpoints に記録
3. 実装kube-proxyEndpoints をノードの iptables/IPVS ルールに変換
  1. Serviceselector: app=web のような条件を宣言します。
  2. endpoints controller がその条件に合い Ready 状態の Pod の IP を探して Endpoints オブジェクトに埋めます。readiness probe を通過していない Pod はここで外れます。
  3. 各ノードの kube-proxy が Endpoints を見て iptables (または IPVS) ルールを敷き、ClusterIP に来たパケットを実際の PodIP のいずれかへ DNAT します。

したがって ClusterIP は何かのプロセスが listen する実際の IP ではなく、kube-proxy がノードのカーネルに仕込んだルールが横取りする仮想 IP です。この点が分かれば ping ClusterIP が通らないのが正常だという事実も自然に飲み込めます。

Service タイプ 4 つ #

タイプアクセス範囲動作主な用途
ClusterIPクラスター内部仮想 IP + DNS、内部負荷分散デフォルト。内部通信
NodePortすべてのノードのポートClusterIP に加えて各ノードの 30000〜32767 ポートを開く外部公開 (シンプル)
LoadBalancer外部 LB IPNodePort に加えてクラウド LB をプロビジョニングクラウドでの外部公開
ExternalNameDNS エイリアスselector なしで CNAME のみ返すクラスター外のサービス参照

上位タイプは下位タイプを含みます。NodePort は ClusterIP をそのまま持ちそこにノードポートを足し、LoadBalancer は NodePort の上にクラウドロードバランサーを乗せます。ExternalName だけは毛色が違います。selector も Endpoints もなく、DNS 段階で外部ドメインの CNAME を返すだけです。

port / targetPort / nodePort の区別 #

3 つのポートフィールドを取り違えると、トラフィックが見当違いの場所へ行きます。

  • port: Service 自身が ClusterIP で開くポート。クライアントは サービス名:port で接続します。
  • targetPort: トラフィックが最終的に届く Pod のコンテナポート。省略すると port と同じ値とみなされます。
  • nodePort: NodePort/LoadBalancer で 各ノードが外部へ開くポート (30000〜32767)。省略すると範囲から自動割り当てされます。

流れで見ると、外部から ノードIP:nodePort → 内部で ClusterIP:port → 最終的に PodIP:targetPort の順序です。

ClusterIP の例 #

最も基本の ClusterIP を YAML で見ます。app=web label を持つ Pod へ 80 番のトラフィックを 8080 番のコンテナポートに転送します。

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP        # 省略してもデフォルト
  selector:
    app: web             # この label を持つ Pod を対象に
  ports:
    - port: 80           # Service が開くポート (ClusterIP:80)
      targetPort: 8080   # Pod のコンテナポート

作った後は、selector が実際に Pod を捕まえたかを Endpoints で確認します。

k get svc web
k get endpoints web        # または k get endpointslices

# クラスター内部から接続テスト (一時 Pod)
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
  wget -qO- http://web:80

k get endpoints web に PodIP が 1 行でも出れば selector が正しくかみ合っています。<none> ならトラフィックの行き先がありません。

NodePort の例 #

外部からノード IP で直接アクセスする必要があるときは NodePort を使います。

apiVersion: v1
kind: Service
metadata:
  name: web-np
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80           # ClusterIP ポート
      targetPort: 8080   # Pod コンテナポート
      nodePort: 30080    # すべてのノードが開く外部ポート (30000〜32767)

これでクラスター内では web-np:80 で、クラスター外では <どれかのノード IP>:30080 で同じ Pod に届きます。nodePort を明示しなければ 30000〜32767 の範囲から自動的に 1 つ選んでくれます。試験で特定のポートを要求されたら必ず明示します。

k get svc web-np
curl http://<ノードIP>:30080

k expose: コマンド 1 行で Service を作る #

試験では YAML を手で書くより k exposek create service の方が速いケースが多いです。既存の Deployment の前に Service を付ける最も速い方法です。

# Deployment を ClusterIP で公開 (selector は Deployment の label から自動抽出)
k expose deployment web --port=80 --target-port=8080

# NodePort で
k expose deployment web --port=80 --target-port=8080 --type=NodePort

# 作る前に YAML だけ確認したいとき
k expose deployment web --port=80 --target-port=8080 $do

k expose の大きな利点は selector を自動で合わせてくれる という点です。手で selector を書いてタイポをすると Endpoints が空になりますが、expose は対象オブジェクトの label をそのまま持ってくるのでそのミスがありません。ただし nodePort を特定の値に固定するオプションはないので、固定が必要なら $do で YAML を抜き出し、nodePort の 1 行を入れて適用します。

headless Service #

clusterIP: None にすると headless Service になります。仮想 IP を受け取らず、DNS 照会時に ClusterIP 1 つではなく 各 Pod の IP を直接 返します。

apiVersion: v1
kind: Service
metadata:
  name: web-hl
spec:
  clusterIP: None        # headless
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

kube-proxy が挟まらないので負荷分散も DNAT もありません。クライアントが DNS から受け取った Pod IP の一覧を見て直接選びます。StatefulSet のように 個々の Pod を固有の名前で指名する必要があるワークロード が headless Service を相方として使います。StatefulSet は #11 で扱いました。

ExternalName: クラスター外のサービスにエイリアスを付ける #

ExternalName は selector も Endpoints もありません。CoreDNS がこの Service 名を外部ドメインの CNAME として返すだけです。

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  type: ExternalName
  externalName: db.prod.example.com

クラスター内のアプリは db.<ネームスペース>.svc.cluster.local を探し、CoreDNS がそれを db.prod.example.com へ案内します。マネージドデータベースのようにクラスター外のエンドポイントを Service 名で抽象化するときに便利です。

運用: Service が動かないときに追う順序 #

CKA のトラブルシューティングで「アプリに接続できない」という問題は、たいていこの経路に沿って切れた場所を見つければ解けます。上から順に確認します。

  1. Service が存在しタイプ/ポートが合っているか
k get svc web
k describe svc web        # Selector、Port、TargetPort を確認
  1. Endpoints に PodIP が埋まっているか。最もよくある失敗ポイントです。
k get endpoints web

ENDPOINTS<none> ならトラフィックの行き先がありません。原因はほぼ 2 つです。

  • selector 不一致: Service の selector と Pod の label が違います。k describe svc の Selector と k get pods --show-labels を並べて比較します。
  • Pod が NotReady: readiness probe を通過していない Pod は Endpoints から外れます。k get pods で READY 列を確認します。
# selector と実際の label を直接照合
k describe svc web | grep -i selector
k get pods --show-labels | grep web
  1. targetPort がコンテナの listen するポートと同じか

Endpoints に IP があるのに接続できないなら、targetPort がコンテナの実際のポートと違うケースが多いです。Endpoints に出た IP:ポートで Pod を直接突きます。

k get endpoints web -o wide
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
  wget -qO- http://<PodIP>:<targetPort>
  1. kube-proxy が動いているか

ClusterIP は通るのに NodePort だけ通らなかったり、ノードごとに結果が違うなら、該当ノードの kube-proxy を疑います。

k get pods -n kube-system -l k8s-app=kube-proxy -o wide
k logs -n kube-system <kube-proxy-pod>

この順序を覚えておくと「Service が動かない」が漠然とした問題ではなく チェックリストのある問題 に変わります。

selector 不一致を作ってみて直す #

最も頻繁なミスを直接再現してみます。Pod label は app=web なのに Service selector を app=webb と誤って書いた状況です。

# Endpoints が空 → selector 不一致を疑う
k get endpoints web
# NAME   ENDPOINTS   AGE
# web    <none>      30s

# selector を正しい label に修正
k patch svc web -p '{"spec":{"selector":{"app":"web"}}}'

# 再び埋まったか確認
k get endpoints web

selector を直した瞬間に endpoints controller が PodIP を再び埋め、kube-proxy がルールを更新します。別途の再起動は不要です。

試験ポイント #

  • Endpoints を先に見る。Service 問題の最初のコマンドはほぼ常に k get endpoints です。空なら selector か readiness を、埋まっているなら targetPort か kube-proxy を疑います。
  • port / targetPort / nodePort を区別するport は Service、targetPort は Pod、nodePort はノードです。3 つを取り違えると誤答に直結します。
  • NodePort 範囲は 30000〜32767。試験で特定の nodePort を要求されたら明示し、範囲を外れると拒否されます。
  • k expose が selector のタイポを防ぐ。既存のワークロードを公開するときは手で selector を書かず、expose で自動抽出する方が速くて安全です。
  • headless は clusterIP: None。仮想 IP なしで PodIP を直接返す動作と StatefulSet との組み合わせを問われます。
  • ClusterIP に ping は通らない。ClusterIP は仮想 IP なので ICMP に応答しません。検証は ping ではなく wget/curl で行います。

まとめ #

この記事で押さえたこと:

  • Service は揺れる Pod の集合の前の安定した仮想 IP・DNS・負荷分散 の抽象化です。
  • 動作は selector (Service) → Endpoints (endpoints controller) → ルール (kube-proxy) の 3 段階です。Service は Pod を直接指しません。
  • タイプは ClusterIP (デフォルト・内部) / NodePort (ノードポート 30000〜32767) / LoadBalancer (クラウド LB) / ExternalName (CNAME) の 4 つで、上位が下位を含みます。
  • ポートは port (Service) / targetPort (Pod) / nodePort (ノード) で区別します。
  • clusterIP: None は headless で PodIP を直接返し、k expose は selector を自動で合わせます。
  • Service が動かないなら Endpoints → selector/readiness → targetPort → kube-proxy の順で追います。

次: Networking 2 #

Service で Pod の集合に安定した入口を作りました。しかし NodePort はポート番号が雑然としていて、LoadBalancer はサービスごとに LB を 1 つずつ立てて高くつきます。HTTP レベルでホスト・パスベースに複数のサービスを 1 つの入口にまとめるツールが必要です。

#19 Networking 2: Ingress、IngressClass、TLS では、Ingress がどのように L7 ルーティングをするのか、IngressClass でどのコントローラーが処理するかを選ぶ方法、そして TLS 証明書を Secret として付けて HTTPS を終端する方法まで運用視点で整理します。

X