本記事には広告(アフィリエイトリンク)が含まれます。

KubernetesのService 4タイプ実践【CKAD第8回】

広告
広告

第8回スコープ・学習目標・今ここマップ

動作確認バージョン: kind v0.31.0 / kindest/node:v1.35.0 / kubectl v1.35.0 (Kustomize v5.7.1) / fanclub-backend:0.1.0 (eclipse-temurin:25-jre + Payara Micro 7.2026.4) / curlimages/curl:8.20.0 / Docker CE 29.4.3 / containerd 2.2.3 / AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-10 時点・k8s-ops 実機検証済・SP_vol1-pre-13 起点)

本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第8回です。第3部「アプリリソース」の第2回として、Service の 4 タイプ(ClusterIP / NodePort / LoadBalancer / ExternalName)・Endpoints / EndpointSlices の自動管理・CoreDNS による Service Discovery・kubectl port-forward を使った開発時アクセス を扱います。

CKAD D5(Services and Networking・20 %)の中核を網羅します。

第7回からの継承状態確認(SP_vol1-pre-13 状態):

項目状態出典
kind クラスタkind-control-plane Ready(v1.35.0・121m old)Lead 実機観察
fanclub-backend Poddefault ns で 1/1 Running(IP 10.244.0.10・37m)Lead 実機観察
fanclub-backend Service未作成(ep8 で作成する)Lead 実機観察
metrics-serverkube-system ns で 1/1 Runningep6 H2-9 で導入済
alias k=kubectl~/.bashrc に永続設定済ep6 / ep7 完了済
~/fanclub-manifests/ディレクトリ作成済(ep7 で初作成)ep7 完了済

今ここマップ(第1巻 19 回中の現在位置):

第1部 コンテナとDocker
    第1回: コンテナ技術概念 + Docker環境準備  [完了]
    第2回: Docker基本操作  [完了]
    第3回: Dockerfile + マルチステージビルド + JDK 25/Payara Micro  [完了]
    第4回: コンテナレジストリ + イメージタグ戦略 + Trivy スキャン  [完了]

第2部 Kubernetes基礎
    第5回: K8s全体像 + kind で軽量K8s  [完了]
    第6回: kubectl基本操作 + Observability・Debug  [完了]

第3部 アプリリソース
    第7回: Pod + Multi-containerパターン  [完了]
  ★ 第8回: Service とネットワーキング  ← 今ここ
    第9回: ストレージ(PVC + StatefulSet)+ PostgreSQL DB追加
    第10回: ConfigMap + Secret + ServiceAccount 基礎
    第11回: Job + CronJob + DaemonSet

第4部 ワークロード戦略(第12〜14回)
第5部 セキュリティ基礎(第15〜16回)
第6部 パッケージ管理 + HTTPS公開(第17〜19回)

第8回を終えると、以下を習得した状態になります。

  • Service がなぜ必要かを「Pod IP の不安定性」の観点から説明できる
  • ClusterIP / NodePort / LoadBalancer / ExternalName の用途と使い分けを説明できる
  • selector / port / targetPort の関係を正確に記述した Service YAML を作成できる
  • <service>.<namespace>.svc.cluster.local の FQDN で CoreDNS がどのように Service を解決するか説明できる
  • Endpoints / EndpointSlices の自動管理の仕組みを kubectl get endpoints で確認できる

模擬アプリ進捗(第8回):第7回でデプロイした fanclub-backend Pod は IP 10.244.0.10 で稼働していますが、この IP は Pod を削除・再作成するたびに変わります。第8回では Service を追加することで安定したアクセス手段を確立します。第9回では PostgreSQL 18 の StatefulSet を追加して 3 層構成を完成させます。

第8回完了後の模擬アプリ状態:fanclub-backend Pod(Running・ep7 から継続)+ fanclub-backend Service(ClusterIP・ep8 で新規作成)。テスト用 curl Pod は演習後にクリーンアップします。fanclub-backend Service は ep9 でも継続使用します。

なぜ Service が必要か — Pod IP は不安定という現実

第7回で fanclub-backend Pod を起動し、IP 10.244.0.10 が割り当てられました。この IP アドレスを直接知っていれば、現時点では Pod にアクセスできます。しかし、この IP に依存した設計は現場では通用しません。

Pod IP は削除・再作成で変わる

Kubernetes において Pod は「使い捨て」の存在です。アプリのデプロイ更新・ノード障害による再スケジューリング・OOMKilled による再起動など、Pod が削除されて新しい Pod が作成される場面は頻繁に発生します。新しい Pod には、以前の Pod とは異なる IP が割り当てられます

クライアント側が Pod IP をハードコードしている場合、Pod が再作成されたタイミングで接続が失敗します。Pod の IP は不変のアドレスではなく、Pod の生存期間に紐づいた一時的なアドレスです。

この問題を実機で確認するため、fanclub-backend Pod を削除して再起動させてみます。

実行コマンド(現在の Pod IP を確認):

$ kubectl get pods -o wide

実行結果:

NAME               READY   STATUS    RESTARTS   AGE   IP            NODE
fanclub-backend    1/1     Running   0          37m   10.244.0.10   kind-control-plane

現在の Pod IP は 10.244.0.10 です。この Pod を削除して、同じ YAML で再作成するとどうなるか確認します(本番環境でこの操作を行う場合は Deployment が管理していることが前提ですが、概念確認のための説明です)。

Pod を削除すると、Kubernetes は Pod を終了させます。単独の Pod(Deployment など上位リソースに紐づいていない Pod)の場合は再作成されません。

再び kubectl apply -f で同じ Pod YAML を適用すると、新しい IP が割り当てられます。Kubernetes の IP アドレス管理(IPAM)は利用可能な範囲からアドレスを割り当てるため、前回と同じ IP になる保証はありません。

Service が解決する問題

Service は Pod IP の不安定性を解決するための抽象レイヤーです。Service を作成すると、Kubernetes は固定の仮想 IP(ClusterIP)を割り当てます。この ClusterIP は Pod の生死に関わらず変わりません。

クライアントは Pod の IP ではなく、Service の ClusterIP(または Service 名)を知っていればよい設計になります。Pod が再作成されて IP が変わっても、Service が自動的に新しい Pod に転送ルールを更新するため、クライアント側の設定変更は不要です。

観点Pod IP 直接アクセスService 経由アクセス
アクセス先Pod の IP(例: 10.244.0.10)Service の ClusterIP(例: 10.96.xxx.xxx)
Pod 再作成後IP が変わるため接続失敗Service が転送先を自動更新するため継続接続
複数 Pod への振り分け不可(IP は 1 Pod に 1 つ)可能(ラベルが一致する複数 Pod に自動負荷分散)
DNS 名でのアクセス不可可能(CoreDNS が Service 名を ClusterIP に解決)

次のセクションで Service が内部でどのように動作するかを詳しく見ていきます。

Service の仕組み — セレクタ・Endpoints・kube-proxy

Service は「ラベル一致した Pod の集合へのネットワーク転送」を担います。ここでは内部動作を段階的に整理します。

転送フローの概要

Service の転送は以下の流れで行われます。

クライアント Pod
    ↓ curl http://fanclub-backend:80
Service ClusterIP (10.96.xxx.xxx:80)
    ↓ kube-proxy が管理する iptables ルール
    ↓ Endpoints: [10.244.0.10:8080]
fanclub-backend Pod (10.244.0.10:8080)

(図解 08-01 参照)

Service 転送フロー図 - Client Pod(curl-test)が curl http://fanclub-backend:80 を実行し、①DNS 解決で CoreDNS が ClusterIP を返す、②kube-proxy が iptables/IPVS の転送ルールを適用、③EndpointSlice が管理する Pod IP リストへ動的転送、という縦フローで ClusterIP Service・EndpointSlice・Backend Pod へ届く流れを示す。

spec.selector によるラベルマッチング

Service の中核は spec.selector フィールドです。Service を作成すると、Kubernetes はクラスタ内の Pod を監視し、spec.selector で指定したラベルと一致する Pod を探します。

第7回で定義した labels.app: fanclub-backend がそのまま ep8 Service の selector.app: fanclub-backend に対応します。

ラベルが一致した Pod の IP とポートが Endpoints リストとして登録されます。Pod が追加・削除されると Endpoints リストも自動更新されます。

kube-proxy の役割

各 Node で動作する kube-proxy が、Endpoints リストをもとに iptables(または IPVS)のルールを管理します。クライアントが ClusterIP にアクセスすると、kube-proxy が設定したルールに従って実際の Pod IP に転送されます。

kube-proxy には iptables モード(デフォルト)と IPVS モードがあります。kind クラスタは iptables モードが標準構成です。CKAD 試験では内部実装の詳細(iptables のルール構造)は問われません。「kube-proxy が ClusterIP → Pod IP の転送ルールを管理する」という概念レベルの理解で十分です。

port と targetPort の違い

Service の YAML には 3 種類のポートフィールドがあります。違いを明確に整理します。

フィールド意味例(fanclub-backend)
portService が受け付けるポート(クライアントが接続するポート)80
targetPortPod のコンテナポート(転送先)8080
nodePortNode が外部に公開するポート(NodePort タイプのみ・30000-32767)30080

fanclub-backend の場合、クライアントは fanclub-backend:80 でアクセスします。Service は内部でこのアクセスを 10.244.0.10:8080(Pod の Payara Micro が Listen しているポート)に転送します。

porttargetPort は同じ値でも構いませんが、外向きポートとコンテナポートを分けることで柔軟な設計が可能になります。

ClusterIP — クラスタ内部通信の基本タイプ

Service のデフォルトタイプは ClusterIP です。spec.type を省略した場合も ClusterIP として動作します。クラスタ内部からのみアクセス可能な仮想 IP が自動割り当てされます。

ClusterIP Service の完全 YAML

fanclub-backend 用の ClusterIP Service の完全な YAML です。

apiVersion: v1
kind: Service
metadata:
  name: fanclub-backend
  namespace: default
  labels:
    app: fanclub-backend
spec:
  type: ClusterIP
  selector:
    app: fanclub-backend
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP

SSOT 照合済みの値:

  • selector.app: fanclub-backend:ep7 HTML L192 の labels.app: fanclub-backend と一致
  • targetPort: 8080:ep7 HTML L206 の containerPort: 8080(Payara Micro の Listen ポート)と一致

ClusterIP の特性と注意点

ClusterIP の仮想 IP は Service 作成時に自動割り当てされます。spec.clusterIP フィールドに明示的に IP を指定することもできますが、推奨しません。

理由は spec.clusterIP不変フィールド(immutable field)であり、Service を削除・再作成すると別の ClusterIP が割り当てられるため、ハードコードした IP が無効になります。クライアントは ClusterIP をハードコードせず、DNS 名(Service 名)でアクセスする習慣をつけてください。

クラスタ外部(ホスト OS)からは ClusterIP に直接到達できません。クラスタ外部からのアクセスには NodePort・LoadBalancer・または kubectl port-forward(開発用)を使います。

NodePort — 外部からの一時的なアクセス手段

NodePort は ClusterIP の機能に加えて、各 Node のポートに外部からアクセスできるタイプです。Node の IP + 指定のポート番号でクラスタ外部からアクセスできます。

NodePort の仕組み

NodePort の転送フローは以下の通りです。

外部クライアント
    ↓ curl http://<Node-IP>:30080
NodePort (各 Node の 30080/TCP)
    ↓ ClusterIP に転送
Service ClusterIP (10.96.xxx.xxx:80)
    ↓ kube-proxy (iptables)
fanclub-backend Pod (10.244.0.10:8080)

NodePort のポート番号は 30000〜32767 の範囲から割り当てられます。spec.ports[].nodePort に明示すると固定できます。省略すると自動割り当てです。

kind 環境での NodePort の制約

本シリーズの kind クラスタは extraPortMappings なしで作成しています(ep5 の設計判断)。そのため、NodePort Service を作成してもホスト OS(k8s-ops)からは直接到達できません。

kind の Control Plane Node(Docker コンテナ)内部では NodePort が機能しますが、Docker のポートマッピングが設定されていないためホスト OS に到達しません。

extraPortMappings の設定は第17回(Gateway API + kind 設定更新)で対応します。ep8 では kubectl port-forward で代替します。

NodePort Service の YAML 例

apiVersion: v1
kind: Service
metadata:
  name: fanclub-backend-nodeport
  namespace: default
spec:
  type: NodePort
  selector:
    app: fanclub-backend
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 30080
      protocol: TCP

NodePort の本番環境での位置づけ

NodePort は 開発・テスト向けの一時的な外部公開手段です。本番環境では以下の理由から推奨されません。

  • 30000-32767 の非標準ポートを外部に公開するため、ファイアウォール設定が煩雑になる
  • Node の IP が変わると(クラウドでの Node 差し替えなど)アクセス先が変わる
  • SSL 終端やリクエストルーティングが Service レベルでは設定できない

本番環境では LoadBalancer または Gateway API(第18回)を使います。

LoadBalancer と ExternalName — 本番と外部サービス連携

Service 4 タイプ比較図 - ClusterIP・NodePort・LoadBalancer・ExternalName を横 4 列で並べ、到達範囲・典型用途・kind での動作の 3 行で比較。ClusterIP はクラスタ内部のみ・Pod 間通信の標準、NodePort は全 Node の指定ポート経由・開発向け一時公開、LoadBalancer はインターネット公開・kind では EXTERNAL-IP が pending、ExternalName は外部サービスを DNS CNAME で包む・セレクター不要、という違いを示す。

LoadBalancer — クラウド環境の外部公開

LoadBalancer タイプは、クラウドプロバイダ(AWS ELB / GCP Cloud Load Balancing / Azure Load Balancer)が外部ロードバランサーを自動払い出しするタイプです。NodePort の機能に加えて、クラウドネイティブな LB が前段に追加されます。

apiVersion: v1
kind: Service
metadata:
  name: fanclub-backend-lb
  namespace: default
spec:
  type: LoadBalancer
  selector:
    app: fanclub-backend
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP

kind 環境では MetalLB がないため、LoadBalancer Service を作成しても EXTERNAL-IP<pending> のままになります。

実行コマンド(kind での確認例):

$ kubectl get svc fanclub-backend-lb

実行結果(kind での典型的な出力):

NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
fanclub-backend-lb   LoadBalancer   10.96.xxx.xxx   <pending>     80:3xxxx/TCP   5s

<pending> の状態は「クラウドプロバイダまたは MetalLB が LB リソースを払い出すのを待っている」状態です。第2巻第9回で MetalLB と組み合わせて EXTERNAL-IP が払い出される実機確認を行います。

ExternalName — クラスタ外の DNS 名への CNAME

ExternalName タイプは、Pod セレクターを持たない特殊な Service です。spec.externalName に外部の DNS 名を指定すると、Service 名への DNS 解決がその外部 DNS 名の CNAME として返ります。

典型的なユースケースは「クラスタ外の DB(RDS など)をクラスタ内の Service 名でアクセスしたい」場合です。

apiVersion: v1
kind: Service
metadata:
  name: external-db
  namespace: default
spec:
  type: ExternalName
  externalName: db.company.example.com

この設定により、クラスタ内の Pod から external-db.default.svc.cluster.local にアクセスすると、CNAME として db.company.example.com が返ります。DB を別のホスト名に移行する際もアプリ側の接続文字列を変更せず、Service の externalName を書き換えるだけで対応できます。

Service 4 タイプの比較

タイプ到達範囲典型用途kind での動作
ClusterIPクラスタ内部のみPod 間通信・マイクロサービス間通信完全動作
NodePortNode IP + ポート(外部から)開発・テスト環境での一時的な外部アクセスService 作成は可能・ホスト OS から直接不可(extraPortMappings なし)
LoadBalancer外部 LB 経由(外部から)本番環境の外部公開(AWS / GCP / Azure)EXTERNAL-IP が <pending> のまま(MetalLB なし)
ExternalNameCNAME として外部 DNS 名に解決クラスタ外 DB・外部 API への透過的アクセスDNS 解決のみ・Pod セレクターなし

Service Discovery と DNS — <service>.<namespace>.svc.cluster.local

Service を作成しただけでは、クライアントは ClusterIP の数値 IP を直接知らなければなりません。Kubernetes は CoreDNS によって Service 名を ClusterIP に解決する DNS 機能を標準で提供します。この仕組みを Service Discovery と呼びます。

CoreDNS の役割

kube-system namespace に kube-dns という ClusterIP Service があります。この ClusterIP(10.96.0.10)が CoreDNS Pod への入口です。各 Pod の /etc/resolv.conf には自動的にこの IP が nameserver として設定されます。

Pod から DNS クエリが発生すると、以下の流れで解決されます。

Pod の DNS クエリ
    ↓ /etc/resolv.conf の nameserver 10.96.0.10 に送信
kube-dns Service(ClusterIP 10.96.0.10:53)
    ↓ CoreDNS Pod に転送
CoreDNS Pod(kube-system namespace で 2 Pod 稼働)
    ↓ Kubernetes API からの Service 情報をもとに応答
Service の ClusterIP を返す

Pod 内の /etc/resolv.conf

Pod 内の /etc/resolv.conf の典型的な内容は以下です。

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
  • nameserver 10.96.0.10:CoreDNS に問い合わせる DNS サーバーの IP
  • search default.svc.cluster.local svc.cluster.local cluster.local:ドット数が少ない名前(例: fanclub-backend)に対して自動補完するドメインのリスト
  • options ndots:5:ドット数が 5 未満の名前は search ドメインを付与してから解決を試みる

search ドメインの仕組みが「短縮形でも解決できる理由」の核心です。Pod から fanclub-backend という名前だけで DNS クエリを送ると、CoreDNS は fanclub-backend.default.svc.cluster.local として解決を試みます。これは search フィールドの最初のエントリ default.svc.cluster.local が自動補完されるためです。

クラスタ内の Pod から curl http://fanclub-backend/ と書けば、クラスタ内 DNS が自動的に Service の ClusterIP に解決してくれます。

FQDN 命名規則の段階的解説

CKAD 試験で頻出の FQDN 形式を整理します。

名前形式適用範囲
fanclub-backend同一 namespace 内(search ドメインの補完が機能)fanclub-backend.default.svc.cluster.local として解決
fanclub-backend.default別 namespace からも解決可能fanclub-backend.default.svc.cluster.local として解決
fanclub-backend.default.svc.cluster.local完全 FQDN(最も確実・namespace をまたぐ場合は必須)FQDN そのもの

CKAD 試験では <service>.<namespace>.svc.cluster.local の FQDN 形式が頻出です。異なる namespace からアクセスする場合は namespace を省略できないため、FQDN または <service>.<namespace> 形式を使います。

Endpoints / EndpointSlices — Service と Pod を繋ぐ仕組み

Service の spec.selector に一致する Pod の IP とポートは Endpoints オブジェクトとして管理されます。Endpoints は Service と同じ名前で自動作成・自動更新されます。

Endpoints の自動管理

Endpoints の自動管理ルールは以下の通りです。

  • Pod が Running かつ Ready 状態になると Endpoints に追加される
  • Pod が NotReady または削除されると Endpoints から自動除外される
  • スケールアウトで Pod が増えると Endpoints に追加され、自動的に負荷分散対象になる

Endpoints の確認コマンドは次の通りです(詳細は演習①で実機確認します)。

実行コマンド:

$ kubectl get endpoints fanclub-backend

Pod が Running 状態であれば 10.244.0.10:8080 のような形式で Pod の IP:Port が表示されます。Service の spec.selector が Pod のラベルと一致しない場合は <none> と表示されます(ヒヤリハット 1 で詳説します)。

EndpointSlice(v1.35 デフォルト)

Endpoints の拡張版として EndpointSlice があります。K8s v1.21 で GA、v1.35 ではデフォルト有効です。

Endpoints は 1 つのオブジェクトにすべての Pod IP をリストするため、大規模クラスタで Pod が数千になると更新コストが大きくなります。EndpointSlice は Pod グループを複数のスライスに分割して管理するため、大規模環境での性能が改善されます。

実行コマンド:

$ kubectl get endpointslices

Service を作成すると自動的に同名の EndpointSlice が作成されます。日常的なトラブルシュートでは kubectl get endpoints で十分ですが、大規模環境では EndpointSlice の確認も有効です。

手動 Endpoints(概念のみ)

セレクターのない Service を作成し、Endpoints オブジェクトを手動で定義することもできます。この方法で、クラスタ外の VM やオンプレミス DB をクラスタ内の Service 名でアクセスできます。

ExternalName との違いは、ExternalName が DNS 名を指定するのに対し、手動 Endpoints では IP アドレスを直接指定します。本回では概念の紹介にとどめ、実機適用は行いません。

Headless Service — StatefulSet への布石

spec.clusterIP: None を指定した Service を Headless Service と呼びます。ClusterIP が割り当てられない特殊な Service で、DNS の挙動が通常の Service と異なります。

Headless Service の DNS 解決

通常の Service では DNS が ClusterIP(仮想 IP)を返します。Headless Service では DNS がセレクターに一致する Pod の実 IP を直接返します(複数の A レコード)。

Service タイプDNS が返す値用途
通常 ClusterIPClusterIP(仮想 IP・1 つ)一般的な Pod 間通信・負荷分散
Headless(clusterIP: NonePod の実 IP(複数の A レコード)StatefulSet の安定した Pod 名でのアクセス

StatefulSet との組み合わせ(ep9 の布石)

第9回で PostgreSQL を StatefulSet として追加するとき、Headless Service を使います。StatefulSet の Pod は <statefulset-name>-<ordinal> という安定した名前で作成されます。

Headless Service と組み合わせると、fanclub-db-0.fanclub-db.default.svc.cluster.local という形式で特定の Pod に直接アクセスできます。PostgreSQL のレプリケーション設定やプライマリ選択で、Pod を名前で識別できることが重要になります。

Headless Service の YAML 例(ep9 の準備として参照のみ):

apiVersion: v1
kind: Service
metadata:
  name: fanclub-db
  namespace: default
spec:
  clusterIP: None
  selector:
    app: fanclub-db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP

本回では概念の確認のみです。Headless Service の実機操作は第9回で実施します。

やってみよう①:ClusterIP Service 作成 → テスト Pod から疎通確認

fanclub-backend 用の ClusterIP Service を作成し、テスト用 Pod から HTTP 疎通を確認します。所要時間の目安は約 15 分です。

CKAD 試験で Service を素早く作成するには --dry-run=client -o yaml テクニックを活用します。まず雛形を生成してからファイルに保存し、必要なフィールドを追記する流れを習慣化します。

前提状態の確認

演習開始前に fanclub-backend Pod が稼働中であることを確認します。

実行コマンド:

$ kubectl get pods -o wide

実行結果:

NAME               READY   STATUS    RESTARTS   AGE   IP            NODE
fanclub-backend    1/1     Running   0          37m   10.244.0.10   kind-control-plane

1/1 Running を確認できたら演習を開始します。

Step 1:–dry-run=client -o yaml で YAML 雛形を生成する

kubectl create service clusterip コマンドで Service YAML の雛形を生成します。

実行コマンド:

$ kubectl create service clusterip fanclub-backend \
    --tcp=80:8080 \
    --dry-run=client -o yaml \
    > ~/fanclub-manifests/fanclub-backend-service.yaml

生成された YAML を確認します。

実行コマンド:

$ cat ~/fanclub-manifests/fanclub-backend-service.yaml

実行結果:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: fanclub-backend
  name: fanclub-backend
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: fanclub-backend
  type: ClusterIP
status:
  loadBalancer: {}

雛形が生成されました。spec.ports[].namehttp に変更し、namespace フィールドと metadata.labels の確認を行います。本演習では雛形をそのまま使いますが、CKAD 試験では必要なフィールドを追記する習慣をつけてください。

creationTimestamp: nullstatus フィールドは apply 後に Kubernetes が管理するため、YAML ファイルに残っていても問題ありません。

Step 2:ファイルを最終版に整形する

生成された雛形を SSOT 照合済みの完全 YAML に置き換えます。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-backend-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: fanclub-backend
  namespace: default
  labels:
    app: fanclub-backend
spec:
  type: ClusterIP
  selector:
    app: fanclub-backend
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP
EOF

Step 3:Service を apply する

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-service.yaml

実行結果:

service/fanclub-backend created

Step 4:Service の状態を確認する

実行コマンド:

$ kubectl get svc fanclub-backend

実行結果:

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
fanclub-backend   ClusterIP   10.96.150.60   <none>        80/TCP    0s

実行コマンド:

$ kubectl describe svc fanclub-backend

実行結果:

Name:                     fanclub-backend
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=fanclub-backend
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.150.60
IPs:                      10.96.150.60
Port:                     http  80/TCP
TargetPort:               8080/TCP
Endpoints:                10.244.0.10:8080
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

describe の出力の Endpoints: フィールドに 10.244.0.10:8080 が表示されていれば、Service が fanclub-backend Pod を正しく検出できています。

Step 5:Endpoints と EndpointSlice を確認する

実行コマンド:

$ kubectl get endpoints fanclub-backend

実行結果:

Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME              ENDPOINTS          AGE
fanclub-backend   10.244.0.10:8080   0s

K8s v1.33 以降、kubectl get endpoints を実行すると Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice という警告が表示されます。これは v1 Endpoints API が非推奨(deprecated)になり、後継の EndpointSlice(discovery.k8s.io/v1)への移行が促されているためです。

10.244.0.10:8080 が表示されていれば Service と Pod の紐づけは正常です。

実行コマンド:

$ kubectl get endpointslices

実行結果:

NAME                    ADDRESSTYPE   PORTS   ENDPOINTS     AGE
fanclub-backend-7frp9   IPv4          8080    10.244.0.10   0s
kubernetes              IPv4          6443    172.18.0.2    154m

Step 6:curl テスト Pod を起動して疎通確認する

curlimages/curl の軽量 Pod を起動し、Service 経由で /health/live にアクセスします。--rm オプションにより Pod は疎通確認後に自動削除されます。

実行コマンド:

$ kubectl run curl-test \
    --image=curlimages/curl:8.20.0 \
    --image-pull-policy=IfNotPresent \
    --rm -it \
    --restart=Never \
    -- curl -s http://fanclub-backend/health/live

実行結果:

{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}
pod "curl-test" deleted from default namespace

{"status":"UP"} が返ってきた場合、Service 経由で fanclub-backend の Liveness エンドポイントへの疎通が確認できています。

curlimages/curl の pull に失敗した場合は、ep7 演習で既に kind ノードに存在する busybox:1.36 で代替できます。

実行コマンド(busybox 代替):

$ kubectl run busybox-test \
    --image=busybox:1.36 \
    --rm -it \
    --restart=Never \
    -- wget -qO- http://fanclub-backend/health/live

Step 7:FQDN でも疎通確認する

完全 FQDN でも同じ ClusterIP に到達できることを確認します。

実行コマンド:

$ kubectl run curl-test2 \
    --image=curlimages/curl:8.20.0 \
    --rm -it \
    --restart=Never \
    -- curl -s http://fanclub-backend.default.svc.cluster.local/health/live

実行結果:

{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}
pod "curl-test2" deleted from default namespace

Service 名の短縮形 fanclub-backend でも FQDN fanclub-backend.default.svc.cluster.local でも、同じ ClusterIP に解決されて疎通が確認できます。これが /etc/resolv.confsearch ドメイン補完の効果です。

演習①完了:ClusterIP Service を作成して、テスト Pod から Service 経由の HTTP 疎通を確認しました。Endpoints に Pod IP が自動登録されていることも確認しました。

やってみよう②:kubectl port-forward でホスト OS から疎通確認

演習②では NodePort Service の概念確認と、kubectl port-forward を使ったホスト OS からの疎通確認を行います。所要時間の目安は約 10 分です。

Step 1:NodePort Service を作成する(概念確認)

NodePort Service を作成して kubectl get svc で nodePort 番号を確認します。kind 環境での制約(ホスト OS から直接到達不可)も確認します。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-backend-nodeport.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: fanclub-backend-nodeport
  namespace: default
spec:
  type: NodePort
  selector:
    app: fanclub-backend
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 30080
      protocol: TCP
EOF

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-nodeport.yaml

実行結果:

service/fanclub-backend-nodeport created

実行コマンド:

$ kubectl get svc

実行結果:

NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
fanclub-backend            ClusterIP   10.96.150.60    <none>        80/TCP         51s
fanclub-backend-nodeport   NodePort    10.96.192.129   <none>        80:30080/TCP   0s
kubernetes                 ClusterIP   10.96.0.1       <none>        443/TCP        155m

NodePort が 30080/TCP で作成されました。kind 環境の Control Plane Node(Docker コンテナ)の IP を確認します。

実行コマンド:

$ kubectl get nodes -o wide

実行結果:

NAME                 STATUS   ROLES           AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         CONTAINER-RUNTIME
kind-control-plane   Ready    control-plane   155m   v1.35.0   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   containerd://2.2.3

kind 環境での制約:本シリーズの kind クラスタは extraPortMappings なしで作成しています。NodePort の 30080 は kind コンテナ(Docker ネットワーク)内では到達可能ですが、ホスト OS(k8s-ops)からは Docker のポートマッピングが設定されていないため直接到達できません。

Step 2:kubectl port-forward で ClusterIP Service に転送する

kubectl port-forward で ClusterIP Service のポート 80 をホスト OS のポート 8080 に転送します。バックグラウンドで実行して curl でアクセスします。

実行コマンド:

$ kubectl port-forward service/fanclub-backend 8080:80 &

実行結果:

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

port-forward がバックグラウンドで実行され、プロンプトが戻ります。

Step 3:ホスト OS から curl でアクセスする

実行コマンド:

$ curl -s http://localhost:8080/health/live

実行結果:

{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}

実行コマンド:

$ curl -s http://localhost:8080/health/ready

実行結果:

{"status":"UP","checks":[]}

注意/api/members エンドポイントは DB(PostgreSQL)との接続が前提です。第9回で DB 演習に到達してから疎通確認します。本回は /health/live/health/ready のみで Service 経由の HTTP 疎通を確認します。

Step 4:port-forward を停止する

実行コマンド:

$ kill %1

または前面に持ってきて Ctrl+C で停止できます。

kubectl port-forward の制限kubectl port-forward は kubectl が API Server との WebSocket トンネルを確立してポート転送を行います。kubectl プロセスが終了すると接続が切れます。

デバッグ・開発用の一時的な手段であり、本番での外部公開手段ではありません。本番環境での外部公開は LoadBalancer または Gateway API(第18回)を使います。

Step 5:NodePort Service を削除する

演習②で作成した NodePort Service を削除します。ClusterIP Service(fanclub-backend)は ep9 で継続使用するため削除しません。

実行コマンド:

$ kubectl delete service fanclub-backend-nodeport

実行結果:

service "fanclub-backend-nodeport" deleted

演習②完了:NodePort Service の概念を確認し、kind 環境での制約(ホスト OS からの直接アクセス不可)を把握しました。kubectl port-forward でホスト OS から ClusterIP Service に転送して疎通確認できました。

やってみよう③:CoreDNS で DNS 解決を確認する

演習①②では「Service 経由で疎通できること」を確認しました。演習③では「その疎通がどのような DNS の仕組みで実現されているか」を実機で詳しく見ていきます。busybox Pod から nslookup で CoreDNS の動作を直接確認します。所要時間の目安は約 7 分です。

Step 1:busybox Pod をインタラクティブに起動する

実行コマンド:

$ kubectl run busybox-dns \
    --image=busybox:1.36 \
    --rm -it \
    --restart=Never \
    -- sh

busybox の / # プロンプトが表示されたら Pod 内シェルに接続できています。

Step 2:Pod の DNS 設定を確認する

実行コマンド(busybox Pod 内):

# cat /etc/resolv.conf

実行結果:

search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

nameserver 10.96.0.10 が CoreDNS の入口(kube-dns Service の ClusterIP)です。search ドメインのリストが「短縮形でも解決できる理由」です。

Step 3:FQDN で DNS 解決する

実行コマンド(busybox Pod 内):

# nslookup fanclub-backend.default.svc.cluster.local

実行結果:

Server:		10.96.0.10
Address:	10.96.0.10:53

Name:	fanclub-backend.default.svc.cluster.local
Address: 10.96.150.60

CoreDNS が fanclub-backend.default.svc.cluster.local を fanclub-backend Service の ClusterIP に解決します。

Step 4:短縮形で DNS 解決する

実行コマンド(busybox Pod 内):

# nslookup fanclub-backend

実行結果:

Server:		10.96.0.10
Address:	10.96.0.10:53

Name:	fanclub-backend.default.svc.cluster.local
Address: 10.96.150.60

** server can't find fanclub-backend.svc.cluster.local: NXDOMAIN

** server can't find fanclub-backend.cluster.local: NXDOMAIN

短縮形 fanclub-backend でも FQDN と同じ ClusterIP に解決されます。これは /etc/resolv.confsearch default.svc.cluster.local ... が自動補完されるためです。

busybox の nslookupsearch ドメインをすべて試すため、最初の 1 件(fanclub-backend.default.svc.cluster.local)で解決成功した後も残りのドメインを試して NXDOMAIN を返します。これは busybox の仕様であり正常動作です。

Step 5:kube-dns の ClusterIP を確認する

実行コマンド(busybox Pod 内):

# nslookup kube-dns.kube-system.svc.cluster.local

実行結果:

Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kube-dns.kube-system.svc.cluster.local
Address 1: 10.96.0.10

kube-dns Service の ClusterIP(10.96.0.10)が返ってきます。これは /etc/resolv.confnameserver と一致しており、CoreDNS そのものが FQDN でアクセスできることを確認できます。

Step 6:busybox Pod を終了する

実行コマンド(busybox Pod 内):

# exit

k8s-ops のシェルプロンプトに戻ります。--rm オプションにより busybox-dns Pod は自動削除されます。

CKAD 試験での重要ポイント

  • FQDN <service>.<namespace>.svc.cluster.local の形式を確実に暗記する
  • 別の namespace からアクセスする場合は namespace を省略できない(<service>.<namespace> 以上が必要)
  • search ドメインの仕組みを理解しておくと DNS トラブルシュートで役立つ

演習③完了:CoreDNS が Service 名を ClusterIP に解決する動作を nslookup で直接確認しました。短縮形と FQDN の両方が同じ ClusterIP に解決されることを実機で把握できました。

現場ヒヤリハット — selector ミスで Endpoints が空 / NodePort 疎通不能の原因

ヒヤリハット 1:selector のラベル typo で Endpoints が空になる

シナリオ:fanclub-backend Service を作成したが、テスト Pod から curl すると接続タイムアウトが発生する。

実行コマンド(原因調査):

$ kubectl describe svc fanclub-backend

実行結果(問題あり):

Name:              fanclub-backend
Namespace:         default
Selector:          app=fanclub-backned
Type:              ClusterIP
IP:                10.96.68.135
Port:              http  80/TCP
TargetPort:        8080/TCP
Endpoints:         <none>

Endpoints: <none> が表示されています。Selector: app=fanclub-backned(typo: “backned” — 末尾 “end” が “ned” になっている)と表示されており、Pod のラベル app=fanclub-backend と一致していません。

実行コマンド(Pod のラベルを確認):

$ kubectl get pods --show-labels

実行結果:

NAME               READY   STATUS    RESTARTS   AGE   LABELS
fanclub-backend    1/1     Running   0          50m   app=fanclub-backend

Pod のラベルは app=fanclub-backend(正しい)ですが、Service の selector は app=fanclub-backned(typo)になっています。selector の typo が原因でした。

根本原因:Service の spec.selector.app: fanclub-backned(”backned” の typo・末尾 “end” → “ned”)が Pod の labels.app: fanclub-backend と不一致。

解決策:Service の YAML を修正して再 apply します。

実行コマンド(Service の YAML 修正後に再 apply):

$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-service.yaml

修正後に再確認します。

実行コマンド:

$ kubectl get endpoints fanclub-backend

Endpoints に Pod IP が表示されれば修正完了です。

予防策と鉄則:Service を apply した後は必ず kubectl describe svc <name>Endpoints フィールドを確認します。<none> の場合は selector のラベルが Pod のラベルと一致していないことを疑います。

Service の selector と Pod のラベルは YAML の別箇所に書くため、typo が発生しやすいポイントです。kubectl get pods --show-labels で Pod 側のラベルを確認し、selector と照合する習慣をつけてください。

ヒヤリハット 2:NodePort 疎通不能(本番 kubeadm 環境向けの先行共有)

シナリオ(第2巻で登場する問題を先行共有):kubeadm クラスタで NodePort Service を作成し、外部のクライアントから http://<Node-IP>:30080 にアクセスを試みるが、接続タイムアウトが発生する。

原因:AlmaLinux 10.1 の firewalld が NodePort のポート(30080/tcp)を遮断しています。kubeadm でクラスタを構築した環境では firewalld がデフォルトで有効です。NodePort の 30000-32767 のポートは自動的には開放されないため、個別に許可ルールを追加する必要があります。

実行コマンド(firewalld の状態確認):

$ sudo firewall-cmd --list-ports

30080/tcp が表示されない場合、firewalld がこのポートを遮断しています。

実行コマンド(NodePort の許可を追加):

$ sudo firewall-cmd --add-port=30080/tcp --permanent
$ sudo firewall-cmd --reload

kind 環境との違い:kind クラスタは Docker ネットワーク上で動作するため、この firewalld 問題は発生しません。しかし第2巻(kubeadm クラスタ・AlmaLinux 10.1)では必ず直面するパターンです。NodePort のトラブルシュートで「疎通できない → まず firewalld を確認」という手順を覚えておいてください。

本番の判断ポイント:本番環境で NodePort の 30000-32767 を全開放することは、セキュリティ上の問題があります。必要な NodePort のみを個別に許可するか、そもそも NodePort を使わず LoadBalancer または Gateway API を採用する設計判断が求められます。

理解度チェック + まとめ + 次回予告 + シリーズ一覧

理解度チェック(○× 形式・全 9 問)

第8回の理解度を確認します。○か×で答えてください。

問 1:Service の type: ClusterIP はデフォルト値であり、spec.type を省略した場合も ClusterIP として動作する。

問 2:Pod を削除・再作成すると IP が変わるため、Service が安定したアクセス先を提供する。

問 3:NodePort の割り当て範囲は 1024〜65535 である。

問 4port: 80, targetPort: 8080 の Service を使う場合、クライアントは 8080 番ポートでアクセスする。

問 5kubectl get endpoints で Service に紐づいている Pod の IP とポートを確認できる。

問 6:クラスタ内の Pod は <service>.<namespace>.svc.cluster.local の形式の FQDN で Service にアクセスできる。

問 7:Headless Service(clusterIP: None)では DNS が ClusterIP(仮想 IP)を返す。

問 8:kind クラスタで type: LoadBalancer の Service を作成すると、EXTERNAL-IP に外部 IP が自動割り当てされる。

問 9kubectl port-forward は本番環境での外部公開手段として使用できる。

解答

解答解説
問 1spec.type を省略すると ClusterIP が適用される。明示する方が可読性は高い
問 2Pod の IP は Pod のライフサイクルに紐づいた一時的なアドレス。Service の ClusterIP は Pod の生死に関わらず固定
問 3×NodePort の範囲は 30000-32767。1024-65535 は一般的な TCP ポートの範囲であり NodePort の範囲ではない
問 4×クライアントは Service の port(80)でアクセスする。targetPort(8080)は Service が Pod に転送するポートであり、クライアントが直接指定する値ではない
問 5kubectl get endpoints <service-name> で Pod の IP:Port のリストを確認できる。<none> の場合は selector のラベル不一致を疑う
問 6CoreDNS が <service>.<namespace>.svc.cluster.local を ClusterIP に解決する。CKAD 試験頻出の FQDN 形式
問 7×Headless Service では DNS が ClusterIP を返さず、セレクターに一致する Pod の実 IP(複数の A レコード)を返す
問 8×kind 環境では MetalLB がないため EXTERNAL-IP<pending> のまま。実際に外部 IP が払い出されるのは MetalLB やクラウドプロバイダの LB コントローラが必要
問 9×kubectl port-forward は kubectl プロセスが終了すると接続が切れるデバッグ・開発専用のツール。本番環境の外部公開には LoadBalancer または Gateway API を使う

第8回まとめ

第8回では以下を実施しました。

  • Service は Pod IP の不安定性を解決し、安定したアクセス先(ClusterIP)を提供する。Pod が再作成されて IP が変わっても Service が転送先を自動更新するため、クライアント側の設定変更が不要になる
  • ClusterIP(クラスタ内部)/ NodePort(外部・開発向け一時手段)/ LoadBalancer(本番・クラウド LB)/ ExternalName(外部 DNS 名への CNAME)の 4 タイプを状況に応じて使い分ける。本番環境では NodePort の代わりに LoadBalancer または Gateway API を使う
  • spec.selector のラベルが Pod のラベルと一致しないと Endpoints が <none> になる。Service apply 後は必ず kubectl describe svcEndpoints フィールドを確認する習慣をつける
  • CoreDNS が <service>.<namespace>.svc.cluster.local の FQDN を ClusterIP に解決する。Pod の /etc/resolv.confsearch ドメイン補完により、同一 namespace 内では Service 名のみで解決できる
  • kubectl port-forward は API Server との WebSocket トンネルを使うデバッグ・開発専用のポート転送手段。kubectl プロセスが終了すると切断される。本番公開には使わない

次回予告

第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加では、PostgreSQL 18 を StatefulSet として追加し、3 層構成(Frontend テスト Pod + Backend + DB)を完成させます。本回確立した fanclub-backend Service(ClusterIP・port 80 → targetPort 8080)は ep9 でも継続使用します。

第8回で概念紹介した Headless Service を StatefulSet と組み合わせて実機で動かします。PV / PVC / StorageClass(kind の standard StorageClass)の仕組みも理解します。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク