はじめに
アプリケーションのデプロイとトラブルシューティングができるようになりましたが、クライアントから確実にアクセスできる仕組みがまだ必要です。Pod の IP アドレスは一時的なものです。Deployment はいつでも Pod を置き換えられるため、置き換え後の Pod には別の IP アドレスが割り当てられる可能性があります。
Kubernetes はこの問題を Service で解決します。Service は変化する Pod の集合を選択し、クライアントに対して安定したネットワーク上の識別子を提供します。この実験では、ラベルから EndpointSlice、クラスタ DNS までの接続経路と、2 種類の Service を順に確認します。ClusterIP はクラスタ内部からのアクセスに、NodePort はノード経由のアクセスに使用します。
この実験では、意図的に Service までを扱います。Ingress は HTTP ルーティングと別のコントローラーを追加するため、まず Service の選択と到達性を具体的に理解しておくと、その仕組みをより簡単に把握できます。
Service のバックエンドをデプロイする
環境の起動: この実験では、完全な Kubernetes クラスターが起動します。コントロールプレーン、ノード、ネットワークコンポーネントの構成には通常 2~3 分かかります。環境の読み込みが完了するまで、そのままお待ちください。
このステップでは、後で Service が公開するレプリケートされたアプリケーションを作成します。ここでいう バックエンド とは、Service 宛てのトラフィックを受信できる Pod のことです。
作業ディレクトリへ移動します。cd はシェルの現在のディレクトリを変更するコマンドで、成功すると通常は何も表示されません。
cd /home/labex/project/service-lab
レプリカを 2 つ持つ Deployment を作成します。app: course-nginx という Pod ラベルは特に重要です。後で Service がこのラベルを選択条件として使用するためです。
シェル構文 cat <<'EOF' > filename は ヒアドキュメント です。終了を示す EOF までの内容がファイルに書き込まれ、> によってファイルが作成または置き換えられます。最初の EOF を引用符で囲むことで、YAML 内でシェルによる意図しない展開が行われるのを防ぎます。
cat <<'EOF' > course-nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: course-nginx
spec:
replicas: 2
selector:
matchLabels:
app: course-nginx
template:
metadata:
labels:
app: course-nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
EOF
適用して、2 つの Pod が起動するまで待ちます。-f は apply が読み込むファイルを指定し、rollout status は Deployment コントローラーの処理完了を待ちます。--timeout=60s は待機時間の上限を設定します。最後のコマンドでは、-l がラベルで絞り込み、-o wide が Pod の IP アドレスとノードの列を追加します。
kubectl apply -f course-nginx-deployment.yaml
kubectl rollout status deployment/course-nginx --timeout=60s
kubectl get pods -l app=course-nginx -o wide
名前付きポート http は、各コンテナの TCP ポート 80 を示すドキュメントとして機能します。2 行のステータスが Running になり、それぞれ異なる Pod IP が表示されるはずです。これらの IP は実際に使用できますが、クライアントが恒久的に利用できるアドレスではありません。次のステップで、Pod の前段に安定した Service の識別子を追加します。
ラベルと Service の選択条件を結び付ける
このステップでは、Service と Pod を結び付けるメタデータを確認します。Service は名前で Deployment を選択するのではなく、セレクターに一致するラベルを持つ Pod を独立して検索します。
Pod のラベルを表示します。-l app=course-nginx はラベルセレクターで、--show-labels は完全なラベル集合を最後の列に追加します。
kubectl get pods -l app=course-nginx --show-labels
各 Pod には app=course-nginx に加えて、自動生成された pod-template-hash があります。Service では、変化しないアプリケーションラベルだけを選択するようにします。
名前、ラベル、IP を簡潔に比較します。-o custom-columns は、指定したオブジェクトのフィールドから表を作成します。各 : の前にある見出しに続いて、その列で使用するフィールドパスを指定します。バックスラッシュは、表示上次の行に続く 1 つのコマンドであることを示します。
kubectl get pods -l app=course-nginx \
-o custom-columns='NAME:.metadata.name,LABEL:.metadata.labels.app,IP:.status.podIP,READY:.status.containerStatuses[0].ready'
生成された名前と IP は個々の Pod を区別する情報で、共通のラベルはそれらの役割を表します。この間接参照によって、Pod が置き換えられても Service は動作し続けます。
Deployment のセレクターと Pod テンプレートのラベルが一致していることを確認します。-o jsonpath='...' は、指定したフィールドだけを抽出します。中かっこの外側にあるテキストはラベルとして出力され、中かっこの内側にあるフィールドパスは値を返し、{"\n"} は改行を挿入します。
kubectl get deployment course-nginx \
-o jsonpath='Selector: {.spec.selector.matchLabels.app}{"\n"}Pod label: {.spec.template.metadata.labels.app}{"\n"}'
どちらの値も course-nginx になるはずです。セレクターが一致していないと、コントローラーまたは Service が意図した Pod から切り離された状態になります。
ClusterIP Service を作成する
このステップでは、デフォルトの Service タイプである ClusterIP を作成します。ClusterIP は、クラスタ内のワークロードから到達できる仮想 IP と DNS 名を提供します。
この実験で紹介したヒアドキュメントのパターンを使ってマニフェストを作成します。
cat <<'EOF' > course-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
ポートの対応関係を注意して確認してください。
port: 80は、クライアントが Service 上で使用するポートです。targetPort: httpは、選択された各 Pod にある名前付きコンテナポートを参照します。- セレクターはバックエンド Pod を選択するものであり、ネットワークアドレスではありません。
Service を検証、適用し、確認します。--dry-run=client を指定すると、リソースを作成せずにローカルで解析できます。これを外すと実際に適用され、続く get service で実際のオブジェクトを読み取れます。
kubectl apply --dry-run=client -f course-nginx-service.yaml
kubectl apply -f course-nginx-service.yaml
kubectl get service course-nginx
CLUSTER-IP の値は Kubernetes によって割り当てられます。バックエンド Pod が変更されても、この Service が存在する間は同じ値が維持されます。
Service から EndpointSlice までを追跡する
このステップでは、Service のセレクターから実際のバックエンドアドレスまでを追跡します。Kubernetes はこれらのアドレスを EndpointSlice オブジェクトに記録します。
Service の詳細を表示します。describe は指定した 1 つのオブジェクトについて、設定、ステータス、関連するエンドポイント情報を詳しく表示するため、簡潔な get の表形式出力の後に確認するのに適しています。
kubectl describe service course-nginx
Selector: app=course-nginx と、ポート 80 上の 2 つの Pod IP を含む Endpoints 行を探してください。
Service 名のラベルで選択される EndpointSlice を一覧表示します。ここでの -l セレクターは、自動的に追加された kubernetes.io/service-name=course-nginx ラベルを使用しています。
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx
アドレスと準備状態を確認します。この JSONPath では range を使い、すべてのエンドポイントに対して囲まれたテンプレートを繰り返し実行します。最初のアドレス、文字列 ready=、準備状態の値、改行の順に出力します。
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
ready=true のアドレスが 2 つ表示されるはずです。接続経路が具体的に確認できました。
Service selector -> matching Pod labels -> EndpointSlice addresses -> ready Pods
Service が存在するにもかかわらずエンドポイントがない場合は、まず Service のセレクターと Pod のラベルおよび準備状態を比較してください。
クラスタ DNS で Service にアクセスする
このステップでは、クラスタ内のクライアントとして動作します。Kubernetes DNS を使えば、同じ名前空間にある Pod から、仮想 IP を覚えておく代わりに Service 名 course-nginx を使用できます。
一時的な BusyBox Pod を起動して NGINX ページを取得し、コマンドの終了時にクライアントを自動的に削除します。オプションを上から順に確認しましょう。
--imageはコンテナイメージを選択し、--image-pull-policy=IfNotPresentはキャッシュ済みのイメージを再利用します。--restart=Neverは、コントローラーが管理するワークロードではなく、単独の Pod を作成します。--rmはコマンド終了後に Pod を削除し、-iはコマンドの入出力を接続したままにします。--区切り文字で kubectl のオプションが終わり、それ以降がコンテナ内で実行するコマンドになります。wget -qO-は URL に静かにリクエストを送り、レスポンス本文を端末へ出力します。
kubectl run service-client \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx
レスポンスには NGINX のウェルカムページが含まれます。トラフィックは特定の Pod IP に直接送られたのではなく、Service を経由しました。
より簡潔な成功確認を実行します。>/dev/null は HTML 本文を破棄し、&& はリクエストコマンドが成功した場合にだけメッセージを表示します。
kubectl run service-client-check \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx >/dev/null && echo "ClusterIP Service responded"
成功メッセージが表示されれば、DNS 名前解決と HTTP 到達性の両方が確認できたことになります。クライアント Pod は一時的なものですが、Service と 2 つのバックエンド Pod は残ります。
NodePort Service を追加する
このステップでは、同じ Pod に対して NodePort タイプの 2 つ目の Service を作成します。NodePort は、各ノードのデフォルト範囲 30000–32767 からポートを開き、そのトラフィックを Service のバックエンドへ転送します。
Kubernetes では、1 つの 複数ドキュメント YAML ファイルから複数のオブジェクトを読み込めます。各オブジェクトは独自の apiVersion、kind、metadata、spec を持ち、--- の行で YAML ドキュメントを区切ります。
既存の ClusterIP Service と新しい NodePort Service を含む再利用可能なファイルを作成します。ClusterIP の定義を繰り返しても問題ありません。同じ望ましい状態を適用すると、既存の設定は変更されないためです。
cat <<'EOF' > course-nginx-services.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: course-nginx-nodeport
spec:
type: NodePort
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080
protocol: TCP
EOF
クラスタを変更する前に、2 つの YAML ドキュメントをまとめて検証します。出力には service/course-nginx と service/course-nginx-nodeport の両方が表示され、その後に (dry run) が続くはずです。
kubectl apply --dry-run=client -f course-nginx-services.yaml
適用して確認します。最初のコマンドは 1 つのファイルから両方のドキュメントを読み込みます。既存の ClusterIP Service は unchanged となり、NodePort Service が作成されます。次のコマンドは、新しく作成された Service を読み取ります。
kubectl apply -f course-nginx-services.yaml
kubectl get service course-nginx-nodeport
PORT(S) 列には 80:30080/TCP と表示されます。80 は Service ポート、30080 はノード側で公開されるポートです。両方の Service は同じ Pod を選択するため、バックエンドアドレスも同じになります。
2 つのアクセス境界を比較する
このステップでは、NodePort をテストし、それぞれの Service タイプをどのような場面で使うべきかを整理します。
Minikube ノードの IP を取得します。$(...) はコマンド置換です。シェルが minikube ip を実行し、その出力を変数 NODE_IP に格納します。-p labex-v135 は準備済みのプロファイルを選択し、echo で保存した値を確認します。
NODE_IP=$(minikube ip -p labex-v135)
echo "$NODE_IP"
ノード側で公開されたポートを通じてアプリケーションにリクエストします。Service の作成を受け付けた後、Kubernetes が新しいノードレベルのネットワークルールを設定するまで数秒かかることがあります。リトライオプションにより、最初の接続拒否で失敗せず、その短い収束時間を待つことができます。
curl -s --retry 5 --retry-connrefused --retry-delay 2 "http://${NODE_IP}:30080" | grep 'Welcome to nginx'
ここで、-s は進行状況メーターを非表示にし、--retry 5 は最大 5 回の再試行を許可します。--retry-connrefused は初期の接続拒否を再試行可能なエラーとして扱い、--retry-delay 2 は試行間隔を 2 秒にします。二重引用符により、URL 内で ${NODE_IP} が展開されます。パイプ | は返された HTML を grep に渡し、証拠となるウェルカム行を表示します。
一致した HTML のタイトルにより、リクエストがバックエンド Pod に到達したことが確認できます。構築した 2 つの経路を比較してください。
in-cluster Pod -> course-nginx:80 -> ready backend Pod
VM/node client -> NODE_IP:30080 -> course-nginx-nodeport:80 -> ready backend Pod
2 つの Service をまとめて確認します。
kubectl get services course-nginx course-nginx-nodeport
クラスタ内部で安定した通信を行う場合は ClusterIP を使用します。ClusterIP はデフォルトの Service タイプであり、他の公開方式の基盤としても一般的です。NodePort はノードレベルの入口を追加するため、学習、開発、外部ロードバランサーとの連携などに役立ちます。どちらも、正しいセレクターと準備状態が ready の EndpointSlice に依存します。
次のチャレンジでは、これらの考え方を自分で応用し、複数の Web ワークロードを公開します。
まとめ
置き換え可能な Pod の前段に、安定したネットワーク上の識別子を構築しました。Service のセレクターを Pod のラベルに結び付け、選択されたバックエンドを EndpointSlice まで追跡し、クラスタ DNS 経由で ClusterIP Service にアクセスし、ノード経由の入口として NodePort を追加しました。
重要な考え方は、Service はアプリケーションそのものではなく、Pod を内包しているわけでもないということです。Service は、選択された準備完了状態のバックエンド集合を継続的に表します。接続に失敗した場合は、Service ポート、セレクター、Pod ラベル、EndpointSlice、バックエンドの準備状態、最後にクライアント側の境界という順番で経路を確認してください。


