- 第8回スコープ・学習目標・今ここマップ
- なぜ Service が必要か — Pod IP は不安定という現実
- Service の仕組み — セレクタ・Endpoints・kube-proxy
- ClusterIP — クラスタ内部通信の基本タイプ
- NodePort — 外部からの一時的なアクセス手段
- LoadBalancer と ExternalName — 本番と外部サービス連携
- Service Discovery と DNS — <service>.<namespace>.svc.cluster.local
- Endpoints / EndpointSlices — Service と Pod を繋ぐ仕組み
- Headless Service — StatefulSet への布石
- やってみよう①:ClusterIP Service 作成 → テスト Pod から疎通確認
- やってみよう②:kubectl port-forward でホスト OS から疎通確認
- やってみよう③:CoreDNS で DNS 解決を確認する
- 現場ヒヤリハット — selector ミスで Endpoints が空 / NodePort 疎通不能の原因
- 理解度チェック + まとめ + 次回予告 + シリーズ一覧
第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 Pod | default ns で 1/1 Running(IP 10.244.0.10・37m) | Lead 実機観察 |
| fanclub-backend Service | 未作成(ep8 で作成する) | Lead 実機観察 |
| metrics-server | kube-system ns で 1/1 Running | ep6 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 参照)

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) |
|---|---|---|
port | Service が受け付けるポート(クライアントが接続するポート) | 80 |
targetPort | Pod のコンテナポート(転送先) | 8080 |
nodePort | Node が外部に公開するポート(NodePort タイプのみ・30000-32767) | 30080 |
fanclub-backend の場合、クライアントは fanclub-backend:80 でアクセスします。Service は内部でこのアクセスを 10.244.0.10:8080(Pod の Payara Micro が Listen しているポート)に転送します。
port と targetPort は同じ値でも構いませんが、外向きポートとコンテナポートを分けることで柔軟な設計が可能になります。
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 — 本番と外部サービス連携

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 間通信・マイクロサービス間通信 | 完全動作 |
NodePort | Node IP + ポート(外部から) | 開発・テスト環境での一時的な外部アクセス | Service 作成は可能・ホスト OS から直接不可(extraPortMappings なし) |
LoadBalancer | 外部 LB 経由(外部から) | 本番環境の外部公開(AWS / GCP / Azure) | EXTERNAL-IP が <pending> のまま(MetalLB なし) |
ExternalName | CNAME として外部 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 サーバーの IPsearch 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 が返す値 | 用途 |
|---|---|---|
| 通常 ClusterIP | ClusterIP(仮想 IP・1 つ) | 一般的な Pod 間通信・負荷分散 |
Headless(clusterIP: None) | Pod の実 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[].name を http に変更し、namespace フィールドと metadata.labels の確認を行います。本演習では雛形をそのまま使いますが、CKAD 試験では必要なフィールドを追記する習慣をつけてください。
creationTimestamp: null と status フィールドは 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.conf の search ドメイン補完の効果です。
演習①完了: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.conf の search default.svc.cluster.local ... が自動補完されるためです。
busybox の nslookup は search ドメインをすべて試すため、最初の 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.conf の nameserver と一致しており、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 である。
問 4:port: 80, targetPort: 8080 の Service を使う場合、クライアントは 8080 番ポートでアクセスする。
問 5:kubectl 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 が自動割り当てされる。
問 9:kubectl port-forward は本番環境での外部公開手段として使用できる。
解答:
| 問 | 解答 | 解説 |
|---|---|---|
| 問 1 | ○ | spec.type を省略すると ClusterIP が適用される。明示する方が可読性は高い |
| 問 2 | ○ | Pod の IP は Pod のライフサイクルに紐づいた一時的なアドレス。Service の ClusterIP は Pod の生死に関わらず固定 |
| 問 3 | × | NodePort の範囲は 30000-32767。1024-65535 は一般的な TCP ポートの範囲であり NodePort の範囲ではない |
| 問 4 | × | クライアントは Service の port(80)でアクセスする。targetPort(8080)は Service が Pod に転送するポートであり、クライアントが直接指定する値ではない |
| 問 5 | ○ | kubectl get endpoints <service-name> で Pod の IP:Port のリストを確認できる。<none> の場合は selector のラベル不一致を疑う |
| 問 6 | ○ | CoreDNS が <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 svcのEndpointsフィールドを確認する習慣をつける- CoreDNS が
<service>.<namespace>.svc.cluster.localの FQDN を ClusterIP に解決する。Pod の/etc/resolv.confのsearchドメイン補完により、同一 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
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージビルド + JDK 25 / Payara Micro イメージビルド
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン
- 第8回 Service とネットワーキング ← 今ここ
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎
- 第11回 Job + CronJob + DaemonSet
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
