Certified Kubernetes Application Developer (CKAD) #18 Service: ClusterIP・NodePort・LoadBalancer・ExternalName

#17 Volumes で Pod にデータをつないだなら、今回はその Pod へ安定して接続する方法を扱います。Deployment が Pod を回している間、Pod は絶えず生まれては消えます。ローリングアップデートが起きると IP がまるごと変わり、ノードが落ちると別のノードで新しい Pod が立ち上がります。このように変化する Pod 群の前に 変わらない入口 を立てるオブジェクトが Service です。

Service は selector で Pod を選び、選んだ Pod の IP 一覧を自動的に追跡しながら、クライアントには固定された名前と仮想 IP 一つだけを公開します。今回の記事では Service の動作原理と 4 つのタイプ、ポート 3 種類の区別、DNS ルール、そして試験によく出るエンドポイントのデバッグまで整理します。

なぜ Service が必要か #

Pod の IP は使い捨てです。Pod が再起動したり別のノードへ移ったりすると IP が変わり、Deployment の replicas が複数あればクライアントはどの IP へ送ればよいかさえ分かりません。そのため Pod IP を直接呼び出すコードはすぐに壊れます。

Service はこの問題を二つの形で解きます。

  • 固定された入口。Service には変わらない名前 (DNS) と ClusterIP が付与されます。後ろの Pod がいくら変わっても、クライアントは同じアドレスへ送ります。
  • 負荷分散。selector に合う複数の Pod へリクエストを自動的に分散します。

Kubernetes の基礎概念をもっと広く押さえたいなら K8s 実務トラック #5 Service とネットワーキング を併せて読むとよいです。この記事は CKAD 実技の観点でコマンドとマニフェストに集中します。

selector と label、そして Endpoints #

Service は Pod を 直接指定しません。 代わりに spec.selector に label 条件を書いておけば、その条件に合う Pod をクラスタが自動で見つけてくれます。この接続を実際に保持しているオブジェクトが Endpoints (または EndpointSlice) です。

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web        # この label を持つ Pod をすべて選択
  ports:
    - port: 80
      targetPort: 8080

上の Service は app=web label を持つ Pod を探し、その Pod たちの IP とポートを Endpoints に自動的に埋めます。Pod が新しく立ち上がると Endpoints に追加され、消えると外れます。この自動管理が Service の核心です。

# Service が実際に指している Pod 一覧を確認
k get endpoints web
k get endpointslices -l kubernetes.io/service-name=web

selector が Pod の label と一文字でも食い違うと Endpoints が空になり、そうなると Service は接続を受けても送り先がありません。この事例は後のデバッグ節で改めて扱います。

Service タイプの 4 種類 #

Service は外部にどこまで公開するかによって 4 つのタイプに分かれます。タイプを指定しなければデフォルト値は ClusterIP です。

タイプアクセス範囲動作主な用途
ClusterIPクラスタ内部仮想 IP 一つを付与、内部からのみアクセス内部マイクロサービス間の通信
NodePortノード IP:ポートすべてのノードの同じポート (30000〜32767) を開く外部公開、LB のない環境でのテスト
LoadBalancer外部 LB IPクラウド LB をプロビジョニング、NodePort を包むクラウドで外部サービスを公開
ExternalNameDNS CNAMEselector なしで外部ドメインへの CNAME だけを返すクラスタ外のサービスに別名を付与

この 4 タイプは 包含関係 として理解すると分かりやすいです。LoadBalancer は NodePort を含み、NodePort は ClusterIP を含みます。つまり NodePort を作ると ClusterIP も一緒に生まれ、LoadBalancer を作ると NodePort と ClusterIP がともに生まれます。ExternalName だけは selector も ClusterIP も持たない別格で、単に DNS 応答を CNAME で返すだけのオブジェクトです。

ClusterIP #

最も基本となるタイプです。クラスタ内部専用の仮想 IP を受け取り、外部からはアクセスできません。マイクロサービス同士が互いを呼び出すときはほぼ常にこのタイプを使います。

NodePort #

すべてのノードで同じポートを開き、ノードの IP とそのポートに入ってきたトラフィックを Service へ渡します。ポート範囲は 30000〜32767 で、指定しなければこの範囲から自動的に一つ割り当てられます。LB のない環境で外部へ素早く公開したり、テストしたりするときに便利です。

LoadBalancer #

クラウドプロバイダ (AWS・GCP・Azure など) の外部ロードバランサをプロビジョニングします。外部 IP が付与され、その IP に入ってきたトラフィックが NodePort を経て Pod へ転送されます。クラウドでない環境では外部 IP が <pending> に留まることがあります。

ExternalName #

selector も ClusterIP も持ちません。代わりに spec.externalName に書いた外部ドメインへ向かう CNAME レコードを返します。クラスタ内のコードが外部データベースのような資源を内部名で呼べるようにするときに使います。

apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  type: ExternalName
  externalName: db.example.com   # この名前への CNAME だけを返す

port vs targetPort vs nodePort #

CKAD で最も紛らわしい箇所がポート 3 種類です。それぞれが指す対象が異なります。

フィールドどこのポートか説明
portService 自身クライアントが Service へ接続するときに使うポート
targetPortPod コンテナService がトラフィックを渡すコンテナポート
nodePortノードNodePort/LoadBalancer でノードが外部へ開くポート (30000〜32767)

流れはこうです。外部またはノードの nodePort に入ってきたトラフィックが Service の port を経て、最終的に Pod の targetPort へ転送されます。targetPort を省略すると port と同じ値とみなされます。コンテナが 8080 で待ち受けているのに Service の port を 80 で開きたいなら、targetPort: 8080 を必ず明示しなければなりません。

命令型で Service を作る #

試験ではマニフェストを手で書くより generator で骨組みを抜く方が速いです。次の二つのコマンドを覚えておけば、大半の Service 作業を処理できます。

# 1) 既存の Deployment を公開 (selector を Deployment label から自動抽出)
k expose deploy web --port=80 --target-port=8080

# 2) Service を直接生成 (selector は直接指定が必要)
k create svc clusterip web --tcp=80:8080
k create svc nodeport web --tcp=80:8080 --node-port=30080

# dry-run でマニフェストの骨組みだけ抜く
k expose deploy web --port=80 --target-port=8080 $do > svc.yaml

k expose は対象ワークロードの label をそのまま selector として持ってくるので、最も手間がかかりません。ただしタイプを変えるには --type=NodePort のようにオプションを足すか、作った後に k edit で直します。

# 公開しながらタイプまで指定
k expose deploy web --port=80 --target-port=8080 --type=NodePort

YAML 例 #

ClusterIP #

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP        # 省略時はデフォルト値
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80           # Service ポート
      targetPort: 8080   # コンテナポート

クラスタ内部の他の Pod は web または web.<namespace> という名前でこの Service へ接続します。

NodePort #

apiVersion: v1
kind: Service
metadata:
  name: web-np
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80           # クラスタ内部で使う Service ポート
      targetPort: 8080   # コンテナポート
      nodePort: 30080    # ノードが外部へ開くポート (30000〜32767、省略時は自動)

この Service はすべてのノードの 30080 ポートに入ってきたトラフィックを app=web Pod の 8080 へ渡します。外部からは <ノード IP>:30080 でアクセスします。

headless Service #

clusterIP: None を指定すると、仮想 IP を受け取らない headless Service になります。この場合、Service 名を DNS で照会すると単一の ClusterIP ではなく 各 Pod の IP 一覧 がそのまま返されます。StatefulSet と一緒に使われ、各 Pod に <pod>.<service>.<namespace>.svc.cluster.local という形の固定 DNS 名を付与するのによく使われます。

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None       # headless
  selector:
    app: db
  ports:
    - port: 5432

クラスタ DNS #

Service を作ると CoreDNS が自動的に DNS レコードを登録します。正式名 (FQDN) は次の形式です。

<service>.<namespace>.svc.cluster.local

たとえば prod ネームスペースの web Service は web.prod.svc.cluster.local で照会されます。同じネームスペース内 の Pod は短い名前である web だけでアクセスでき、別のネームスペースの Service を呼ぶときは最低限 web.prod のようにネームスペースを付けます。

# 一時 Pod から DNS を確認
k run tmp --image=busybox --rm -it --restart=Never -- nslookup web.prod.svc.cluster.local

# 同じネームスペースなら短い名前で
k run tmp --image=busybox --rm -it --restart=Never -- wget -qO- web

デバッグ: エンドポイントが空になる場合 #

試験で「Service へ接続できない」というタイプが出たら、まず最初に Endpoints を確認します。Endpoints が空なら Service が指す Pod を見つけられなかったという意味で、ほぼ常に selector と Pod label の不一致 が原因です。

# 1) Endpoints に Pod IP が埋まっているか
k get endpoints web

# 2) Service の selector を確認
k describe svc web | grep -i selector

# 3) 実際の Pod の label を確認
k get pods --show-labels

たとえば Service の selector が app: web なのに Pod の label が app: webapp なら、二つの値が違うので Endpoints が空になり接続も失敗します。このときは Service の selector を直すか、Pod の label を合わせます。

# Pod label を Service selector に合わせる
k label pod web-xxxxx app=web --overwrite

エンドポイントが空になるもう一つの原因は、targetPort がコンテナの実際の listen ポートと異なる場合、または readiness probe が失敗して Pod が NotReady になり Endpoints から除外される場合です。Endpoints が空なのか、埋まっているがポートが違うのかをまず切り分けると、原因を素早く絞り込めます。

# Service まで到達するか一時 Pod で確認
k run probe --image=busybox --rm -it --restart=Never -- wget -qO- web:80

試験ポイント #

  • タイプのデフォルト値は ClusterIP。NodePort・LoadBalancer は ClusterIP を含み、ExternalName だけが selector と ClusterIP を持たない CNAME 専用です。
  • ポート 3 種類を正確に区別port は Service、targetPort はコンテナ、nodePort はノードです。nodePort の範囲は 30000〜32767 です。
  • k expose deploy ... --port= --target-port= が最も速い公開コマンドです。selector を自動的に持ってきます。
  • headless は clusterIP: None。StatefulSet と一緒に Pod ごとの DNS を作ります。
  • DNS は <service>.<namespace>.svc.cluster.local。同じネームスペースは短い名前で十分です。
  • 接続失敗は k get endpoints から。空なら selector と label の不一致を疑います。

まとめ #

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

  • Service は変化する Pod 群の前の固定された入口であり、selector で Pod を選んで Endpoints を自動管理します。
  • 4 つのタイプ (ClusterIP・NodePort・LoadBalancer・ExternalName) は公開範囲が異なり、前の 3 つは包含関係です。
  • ポートは port (Service)・targetPort (コンテナ)・nodePort (ノード) の 3 種類に分かれます。
  • headless Service とクラスタ DNS のルール、そしてエンドポイントが空になるデバッグまでコマンドで身につけました。

次へ — Ingress と NetworkPolicy #

Service で入口は立てましたが、外部 HTTP トラフィックを経路ごとにルーティングしたり Pod 間の通信を塞いだり開いたりする作業は Service だけでは足りません。

#19 Ingress と NetworkPolicy では、複数の Service をホスト・経路ルールで束ねる Ingress、TLS 終端、そして default-deny から始めて ingress・egress ルールで通信を制御する NetworkPolicy を YAML と kubectl で扱います。

X