第09章:Serviceで“つなぐ”(サービスディスカバリ)🧷🧠✨
この章は、**「PodのIPがコロコロ変わっても、アプリ同士を安定してつなぐ方法」を身につける回です!😆 Kubernetesの世界だと、“つなぎ先はIPじゃなくて名前で呼ぶ”**が基本になります📛🔗
ちなみに本日(2026-02-13)時点のKubernetesは **v1.35.1(2026-02-10リリース)**です。(Kubernetes) この章の内容(Service / DNS / EndpointSlice)は、まさに現行の中心機能です💪(Kubernetes)
9.1 まず結論:Serviceは「固定の入り口(住所&電話番号)」📞🏠
Podは落ちたり増えたりして、IPが変わるのが通常運転です😇💥 だから PodのIPに直打ちすると、すぐ壊れます🫠
そこで登場するのが Service です🧷
- Service:安定した入口(固定の名前&仮想IP)📌
- Pod:中身(入れ替わること前提)♻️
- Label / Selector:どのPodを“中身”として束ねるかの紐づけ🏷️
Kubernetes公式の「Serviceの概念ページ」でも、この思想がど真ん中です。(Kubernetes)
9.2 Serviceディスカバリって何?🤔➡️😎
**Serviceディスカバリ = “サービスを見つける仕組み”**です🧭✨ Kubernetesでは主に DNS で見つけます📡
dbというServiceを作る- アプリは
db(名前)で接続する - DNSが
dbを解決して、Serviceに到達する
Kubernetesは ServiceやPodにDNSレコードを作る仕組みを持っていて、Pod内から「名前で引ける」ようにしてくれます。(Kubernetes)
DNS名のルール(ざっくり)👇
- 同じNamespaceなら:
dbだけでOKなことが多い👍 - 別Namespaceなら:
db.<namespace>やdb.<namespace>.svc.cluster.localが必要になることがある🌍 (この仕組み自体がKubernetesの基本仕様です)(Kubernetes)
9.3 Serviceの種類(超ざっくり使い分け)🧰✨
よく使うのはこのへんです👇(名前だけでも覚えれば勝ち🏆)
- ClusterIP(基本これ):クラスタ内部だけの入口🏠
- NodePort:各ノードのポートを開けて外から入れる🚪
- LoadBalancer:クラウドのLBを使って外から入れる🌩️
- ExternalName:外部のDNS名へのエイリアス👻
- Headless(ClusterIPなし):Podを直接見せたい時(Stateful系で多い)🧱
Serviceの公式ドキュメントにまとまっています。(Kubernetes) ※ この章はまず ClusterIP を主役にします🥳
9.4 Serviceの裏側:EndpointSliceが“実体の名簿”📇🧠
Serviceは「入口」です。 でも実際にどのPodへ流すかは、EndpointSlice が持っています📇✨
ざっくり図にすると👇
- Service(入口) → EndpointSlice(名簿) → PodIP:Port(本体)
EndpointSliceは Serviceをスケールさせるための仕組みとして公式に説明されています。(Kubernetes) また、EndpointsからEndpointSliceへ移行が進んでいて、Service周りの新機能はEndpointSliceが前提になっています。(Kubernetes)
9.5 ハンズオン:API → DB(っぽいもの)を“名前で”つなぐ🔗🍔🗄️
ここから手を動かします✋✨ ゴールはこれ👇
apiがdbという名前で接続する- Podが入れ替わっても壊れない
- つながらない時に「どこを見るか」まで分かる
手順A:Namespaceを作る(迷子防止)🧭📁
apiVersion: v1
kind: Namespace
metadata:
name: demo
適用👇
kubectl apply -f namespace.yaml
手順B:DB(今回はPostgreSQL)+ Service db を作る🐘🧷
ここは「つなぐ練習」なので、DBをDeploymentで置きます🙆♂️ “ちゃんとしたDB運用”は後半(StatefulSetやPVC)でやる想定です🧱💾
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:17
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
value: postgres
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_DB
value: appdb
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: demo
spec:
type: ClusterIP
selector:
app: db
ports:
- name: postgres
port: 5432
targetPort: 5432
適用👇
kubectl apply -f db.yaml
kubectl -n demo get pods,svc
ここで svc/db ができていればOKです🎉
手順C:API(Node/TS)+ Service api を作る🍔🧷
Nodeは本日(2026-02-13)時点で v24がActive LTS です。(nodejs.org) なのでサンプルは Node 24 を基準にします🚀
① APIの最小コード(TypeScript)✍️✨
src/server.ts
import express from "express";
import { Client } from "pg";
const app = express();
const port = Number(process.env.PORT ?? "3000");
// Kubernetes Service名でつなぐのがポイント!
const dbHost = process.env.DB_HOST ?? "db";
const dbUser = process.env.DB_USER ?? "postgres";
const dbPass = process.env.DB_PASS ?? "postgres";
const dbName = process.env.DB_NAME ?? "appdb";
function createClient() {
return new Client({
host: dbHost,
user: dbUser,
password: dbPass,
database: dbName,
port: 5432,
});
}
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.get("/health/db", async (_req, res) => {
const client = createClient();
try {
await client.connect();
const r = await client.query("SELECT 1 AS ok");
res.json({ ok: true, db: r.rows[0] });
} catch (e: any) {
res.status(500).json({ ok: false, error: String(e?.message ?? e) });
} finally {
await client.end().catch(() => {});
}
});
app.listen(port, () => {
console.log(`api listening on :${port} (db host: ${dbHost})`);
});
package.json(最小)
{
"name": "k8s-service-demo",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch --enable-source-maps dist/server.js",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.19.2",
"pg": "^8.13.0"
},
"devDependencies": {
"typescript": "^5.8.0"
}
}
tsconfig.json(最小)
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
② Dockerfile(シンプルに)🐳📦
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci || npm i
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
ここまで作ったら、いつもの流れでイメージを作って(前章までのやり方でOK)レジストリに置く想定です📦🚚 (kindなら
kind load docker-image ...でもOKな構成にできます👍)
③ Kubernetesマニフェスト(Deployment + Service)📄🧷
api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: YOUR_REGISTRY/your-api:1.0.0
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
- name: DB_HOST
value: "db" # ← Service名で接続!
- name: DB_USER
value: "postgres"
- name: DB_PASS
value: "postgres"
- name: DB_NAME
value: "appdb"
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: demo
spec:
type: ClusterIP
selector:
app: api
ports:
- name: http
port: 3000
targetPort: 3000
適用👇
kubectl apply -f api.yaml
kubectl -n demo get pods,svc
手順D:つながったか確認する✅🎯
① まずログを見る👀🪵
kubectl -n demo logs deploy/api
db host: db と出ていれば意図通りです👍
② Podの中から db が引けるか(DNSチェック)📡🔍
Kubernetes公式でも、DNSトラブルは「クラスタ内から確認する」のが王道です。(Kubernetes)
kubectl -n demo run -it --rm debug --image=busybox:1.36 --restart=Never -- sh
中で👇
nslookup db
nslookup db.demo.svc.cluster.local
もし
nslookupが無い/動かない感じなら、ネットワーク調査ツール盛り盛りのnetshootを使うのも定番です🧰 (KubernetesでもDockerでもよく使われます)(GitHub)
kubectl -n demo run -it --rm net --image=nicolaka/netshoot --restart=Never -- bash
中で👇
dig db.demo.svc.cluster.local
③ api をローカルから叩く(port-forward)🚇💻
kubectl -n demo port-forward svc/api 3000:3000
別ターミナルで👇
curl http://localhost:3000/health
curl http://localhost:3000/health/db
/health/db が ok: true なら勝ち🎉🎉🎉
9.6 つながらない時の“型”🧯🥋(ここが超重要)
Service周りは、だいたい事故パターンが決まってます😎✨ 順番に潰せばOK!
パターン1:ServiceのselectorがPodのlabelとズレてる🏷️❌
確認👇
kubectl -n demo get svc db -o yaml
kubectl -n demo get pods --show-labels
selectorの app: db と、Podの labelの app=db が一致してないと、Serviceの中身が空になります🫠
パターン2:Serviceに“中身”がいない(EndpointSliceが空)📇❌
確認👇(Service名で絞る)
kubectl -n demo get endpointslices -l kubernetes.io/service-name=db
kubectl -n demo describe svc db
EndpointSliceがServiceのバックエンド(到達先)を表します。(Kubernetes)
パターン3:port / targetPort を間違えた🔌😵
ありがち👇
port: 5432なのにtargetPort: 15432とか- アプリ側が
DB_HOST=db:5432じゃなくて変なポートを見てる
確認👇
kubectl -n demo describe svc db
kubectl -n demo describe pod -l app=db
パターン4:Namespaceを間違えた📁😇➡️😱
demoにServiceがあるのにdefaultのPodからdbを引いてる
対策:別Namespaceなら FQDN を使う(db.demo.svc.cluster.local)🌍(Kubernetes)
パターン5:DNS自体が壊れてる(CoreDNS)📡💥
Kubernetes公式のDNSデバッグ手順が用意されています。(Kubernetes) まずはこれ👇
kubernetes.defaultが引けるか(超基本)db.demo.svc.cluster.localが引けるか(今回の本題)
9.7 ちいさい課題(5〜15分)📝✨
-
apiをreplicas: 5に増やしてみる📈kubectl -n demo get pods -l app=api -o wideで増えたの確認👀
-
dbPodを消してみる😈kubectl -n demo delete pod -l app=db- IPが変わっても、Service名
dbでつながり続けるのを確認🔁
-
わざと
selectorを壊して「つながらない」を作る🧨- そして describe / endpointslice / nslookup で復旧する🧯
9.8 AIに手伝ってもらうコツ🤖🪄(超おすすめ)
- 「このService、selectorとlabel合ってる?合ってないなら具体的にどこ?」🕵️♂️
- 「
kubectl describe svc dbの出力貼る→原因候補を3つ+確認コマンドも」🔍 - 「
api.yamlを“安全寄りの初学者向け”に整えて(コメント付き)」📝 - 「port/targetPort/コンテナportの関係を図で説明して」🧠📈
※ ただしAIは“それっぽいYAML”を平気で出すので、必ず kubectl describe と get endpointslices で裏取りしましょ✅😎
まとめ🎉
- PodはIPが変わる → 名前で呼ぶのが正解📛
- Service が「安定した入口」になり、DNSで見つけられる📡(Kubernetes)
- 裏側は EndpointSlice が名簿を持ってる📇(Kubernetes)
- つながらない時は「selector」「EndpointSlice」「DNS」「port」を順に見る🥋🧯
次の章(10章)で、Label/Selector/Namespaceをさらに“整理整頓スキル”として固めると、迷子率が激減します🧹🧭✨