📁GitHubにコード公開: GitHub: kubernetes_minikube
シリーズ記事一覧
- 第1回:全体像と環境構築
- 第2回:Pod・ReplicaSet・Deployment
- 第3回:Service・Ingress
- 第4回:ConfigMap・Secret・PersistentVolume
- 第5回:模擬プロジェクト(Web+API+DB+Redis)
- 第6回:トラブルシュートと運用
- 第7回:総まとめと次のステップ
📑 目次
- この記事について
- この記事のゴール
- 前提条件
- プロジェクト概要:k8s-todo
- アーキテクチャ設計
- 新しい概念:Namespace
- 新しい概念:Probe(ヘルスチェック)
- 新しい概念:resources(リソース制限)
- Step 1:基盤(Namespace + ConfigMap + Secret)
- Step 2:DB層(StatefulSet + Headless Service)
- Step 3:Redis層(Deployment + Service)
- Step 4:API層(Deployment + Service)
- Step 5:Web層(Deployment + Service + nginx設定)
- Step 6:Ingress(外部公開)
- 全体の通信フローと動作確認
- お片付けとNamespace削除の注意点
- まとめ
1. この記事について
1-1. シリーズ概要
Docker Compose経験者が「素のKubernetes」を
1週間で実践レベルまで習得することを目指す学習記録です。
1-2. シリーズ構成
| 回 | テーマ | 内容 |
|---|---|---|
| 第1回 | 全体像と環境構築 | Docker Composeとの対比、minikube導入 |
| 第2回 | Pod と Deployment | コンテナ起動〜スケーリング〜ローリングアップデート |
| 第3回 | Service と Ingress | ネットワークと外部公開 |
| 第4回 | ConfigMap / Secret / PV | 設定管理と永続化 |
| 第5回(本記事) | 模擬プロジェクト | 全概念を組み合わせて実践 |
| 第6回 | トラブルシュートと運用 | エラー対応、ログ確認 |
| 第7回 | 総まとめ | 振り返り |
1-3. 対象読者
- 第1回〜第4回を読み終えた方
- 個別の概念は理解したが「全部組み合わせるとどうなるの?」を体験したい方
- Docker Composeで複数コンテナ構成を作ったことがある方
2. この記事のゴール
| # | ゴール | 確認方法 |
|---|---|---|
| ① | 4層構成(Web+API+DB+Redis)を k8sで構築できる |
全Podが Running で外部アクセス可能 |
| ② | 第1〜4回の全リソースの関係性を 実践で理解する |
各YAMLがどのリソースを参照しているか 説明できる |
| ③ | 構築順序の設計判断ができる | なぜDB→Redis→API→Web→Ingress の順か説明できる |
| ④ | Probe・resources等の新概念を実践で使える | ヘルスチェックとリソース制限の 設計意図を語れる |
| ⑤ | Namespace削除時のデータ影響を理解する | ReclaimPolicyとの関係を語れる |
3. 前提条件
3-1. 環境情報
| 項目 | バージョン / 詳細 |
|---|---|
| OS | Windows 11 + WSL2 Ubuntu |
| minikube | インストール済み(第1回で構築) |
| kubectl | インストール済み(第1回で構築) |
| Ingress Controller | 有効化済み(第3回で設定) |
3-2. 前提知識
第1回〜第4回の全内容。特に以下が重要だと思います。
| 概念 | 学んだ回 | 本記事での使用場面 |
|---|---|---|
| Deployment / labels / selector | 第2回 | Web, API, Redis の構築 |
| Service(ClusterIP) | 第3回 | 全コンポーネント間の通信 |
| Ingress | 第3回 | 外部公開 |
| ConfigMap(環境変数 + ファイルマウント) | 第4回 | 設定管理 + nginx.conf |
| Secret | 第4回 | DBパスワード |
| StatefulSet + Headless Service + PVC | 第4回 | PostgreSQL |
4. プロジェクト概要:k8s-todo
4-1. 基本情報
| 項目 | 内容 |
|---|---|
| アプリ名 | k8s-todo(タスク管理API) |
| 構成 | Web(nginx)+ API(サンプル)+ DB(PostgreSQL)+ Redis(キャッシュ) |
| 目的 | 第1〜4回の全リソースを1つのプロジェクトで使う |
4-2. コンポーネント一覧
| コンポーネント | イメージ | k8sリソース | レプリカ数 | 理由 |
|---|---|---|---|---|
| Web | nginx:1.21 | Deployment | 2 | 静的配信 + リバースプロキシ |
| API | hashicorp/http-echo | Deployment | 2 | 軽量HTTPサーバー |
| DB | postgres:14 | StatefulSet | 1 | データ永続化 + 固定DNS名 |
| Redis | redis:7-alpine | Deployment | 1 | キャッシュ(ステートレス扱い) |
🔰 リバースプロキシとは?
ユーザーからのリクエストを
代わりに受け取って、裏側のサーバーに転送する役割のこと。
飲食店で例えると、受付カウンターのようなもの。
お客さん(ユーザー)はカウンター(nginx)に注文を出し、
カウンターが厨房(APIサーバー)に伝えます。
お客さんは厨房の場所や数を知らなくてOKです。ユーザー → [nginx (リバースプロキシ)] → APIサーバー ↑ ここが受付窓口なぜ直接APIに繋がないのか?
- 静的ファイル(HTML/CSS/JS)はnginxが直接返す → APIの負担を減らせる
- APIサーバーが複数台でも、nginxが1つの入口になる
- URLのパスで振り分けられる(
/→ 静的ファイル、/api→ APIサーバー)
4-3. Docker Composeとの対比
Docker Composeなら1ファイルで書ける構成を、k8sでは14個のYAMLファイルに分離します。
「なぜそんなに増えるの?」を対比表で見てみましょう。
| Docker Compose の記述 | k8s で必要になるファイル | 理由 |
|---|---|---|
| ― (暗黙のデフォルトネットワーク) | namespace.yaml |
環境の論理分離 |
environment: |
configmap.yaml |
設定を外出し |
environment:(機密値) |
secret.yaml |
機密情報を分離 |
services: db: |
db/statefulset.yaml |
Pod名・Volume固定 |
services: db: の公開ポート |
db/headless-service.yaml |
Pod個別のDNS名 |
services: redis: |
redis/deployment.yaml + redis/service.yaml
|
ワークロードと通信経路を分離 |
services: api: |
api/deployment.yaml + api/service.yaml
|
同上 |
services: web: |
web/deployment.yaml + web/service.yaml
|
同上 |
| ― (nginx.confを直接バインドマウント) | web/configmap-nginx.yaml |
nginx設定をk8s管理下に |
| ― (HTMLを直接バインドマウント) | web/configmap-html.yaml |
静的ファイルをk8s管理下に |
ports: ["80:80"] |
ingress.yaml |
外部公開のルーティング |
🔰 なぜ分けるの?
Docker Composeは「1つのファイルで全部書ける手軽さ」が強み。
k8sは**「責務ごとにファイルを分ける」設計思想です。
面倒に見えますが、本番運用では「Secretだけ別管理」「Deploymentだけ更新」といった部分的な変更・権限管理**ができるメリットがあります。
5. アーキテクチャ設計
5-1. 全体構成図(概要)
まず、リクエストがどの層を通るかの通信の流れだけを押さえます。
🍽️
お客さん → 受付(Ingress)→ ホール(Web)→ 厨房(API)
→ 冷蔵庫(Redis)/ 食材倉庫(DB)
⚠️ 注意: 本模擬PJのAPI層には
hashicorp/http-echo(固定文字列を返すだけのイメージ)を使用しています。図中のAPI→Redis・API→DBの通信は本番構成を想定した設計図であり、http-echo自体は実際にはRedis・DBに接続しません。
5-1-1. 詳細構成図(k8sリソース14個)
概要図の各層を「k8sリソース」に展開すると、こうなります。
💡実線(→) = 通信の流れ | 点線(-.->) = 設定の参照
点線で繋がっているConfigMap / Secret / PVCは、
各層のPodがマウントや環境変数として参照する設定リソース
⚠️ 注意: 詳細図中のAPI→Redis・API→DBの矢印は本番構成を想定した設計です。模擬用の
http-echoは実際にはこれらに接続しません。
5-2. 全リソースと学習対応マップ
| リソース | 用途 | 学んだ回 |
|---|---|---|
| Namespace | プロジェクト分離 | 第5回(新規) |
| Deployment | Web(×2), API(×2), Redis(×1) | 第2回 |
| StatefulSet | PostgreSQL(×1) | 第4回 |
| ClusterIP Service | web, api, redis の内部通信 | 第3回 |
| Headless Service | PostgreSQL の個別Pod接続 | 第4回 |
| Ingress | 外部公開 | 第3回 |
| ConfigMap(環境変数) | 共通設定 | 第4回 |
| ConfigMap(ファイルマウント) | nginx.conf, index.html | 第4回 |
| Secret | DB パスワード | 第4回 |
| PVC(volumeClaimTemplates) | PostgreSQL データ永続化 | 第4回 |
| labels/selector | 全リソースの紐づけ | 第2回 |
| CoreDNS | Service名での通信 | 第3回 |
5-3. 構築順序
「使われる側」から先に作ります。まず全体の流れ、次に各ステップの中身を見ましょう。
① なぜこの順番? ― 依存関係
🍽️ レストラン開店準備と同じ
食材倉庫・冷蔵庫(データ層)→ 厨房・ホール(アプリ層)→ 看板を出す(公開)
② 各ステップで何を作る?
同じ枠内のリソースは並行して作成可能(DB と Redis は互いに依存しない)
5-4. この記事で作成するファイル(k8sリソース14個)
~/k8s-todo/
├── namespace.yaml
├── configmap.yaml
├── secret.yaml
├── db/
│ ├── headless-service.yaml
│ └── statefulset.yaml
├── redis/
│ ├── service.yaml
│ └── deployment.yaml
├── api/
│ ├── service.yaml
│ └── deployment.yaml
├── web/
│ ├── configmap-nginx.yaml
│ ├── configmap-html.yaml
│ ├── service.yaml
│ └── deployment.yaml
└── ingress.yaml
ここから、模擬プロジェクトを構築するために必要な3つの新概念を先に学びます。
| 章番号 | 新概念 | なぜ必要? |
|---|---|---|
| 6 | Namespace | プロジェクトのリソースを分離するため |
| 7 | Probe(ヘルスチェック) | Podの異常を自動検知・自動復旧するため |
| 8 | resources(リソース制限) | 1つのPodがNode全体を食い尽くさないため |
🍽️ 開店前の準備:
→ 店舗スペースの確保(Namespace)
→ スタッフの健康管理体制(Probe)
→ 調理台の割り当て(resources)
6. 新しい概念:Namespace
6-1. Namespaceとは
| 項目 | 内容 |
|---|---|
| 何者? | k8sクラスタ内の論理的な仕切り |
| 何を分離? | リソース名の空間。同名リソースもNamespaceが違えば共存可能 |
| デフォルト |
default(何も指定しないとここに作られる) |
🍽️
Namespace = フードコートの各店舗スペース。
同じ「メニュー表」でも店舗が違えば別物。
6-2. 主要なNamespace
| Namespace | 用途 |
|---|---|
default |
ユーザーリソースのデフォルト |
kube-system |
k8sシステムコンポーネント(CoreDNS, kube-proxy等) |
kube-public |
全ユーザーに公開される情報 |
ingress-nginx |
Ingress Controller |
6-3. なぜ専用Namespaceを作るか
| メリット | 説明 |
|---|---|
| リソースの整理 | プロジェクトごとに分離 |
| 一括削除 | Namespace削除で中のリソースが全て消える |
| アクセス制御 | RBACでNamespace単位に権限設定 |
| リソースクォータ | Namespace単位でCPU/メモリの上限設定 |
7. 新しい概念:Probe(ヘルスチェック)
7-1. 3種類のProbe
| Probe | 質問 | 失敗時の動作 | 比喩 |
|---|---|---|---|
| 📋 readinessProbe | 「リクエスト受けられる?」 | Serviceの 振り分けから外す |
「体調不良 → シフトから外す」 |
| 💓 livenessProbe | 「生きてる?」 | Podを再起動 | 「倒れた → 起こす(強制)」 |
| 🚀 startupProbe | 「起動できた?」 | Podを再起動 | 「研修中 → 完了まで他チェック停止」 |
7-1-1. 全体シーケンス図
7-1-2. 全体シーケンス図の分割版:フェーズ1~3
Phase 1:🚀 起動チェック(①〜④)
Phase 2:📋 初回登録(⑤〜⑥)
Phase 3:💓📋 通常運用(⑦〜⑩)
7-2. 3つのチェック方式
| 方式 | やること | 使う場面 |
|---|---|---|
| exec | コンテナ内でコマンド実行 | DB(pg_isready)、Redis(redis-cli ping) |
| httpGet | HTTP GETリクエスト | Webアプリ、API(/healthz) |
| tcpSocket | TCPポートに接続 | ポートが開いてるかだけ確認 |
🍽️
exec = シェフに直接聞く。
httpGet = 内線で確認。
tcpSocket = 電気がついてるか見るだけ。
7-3. ⚠️ 実務でよくあるミス ― livenessProbeにDB接続を入れてはいけない
Probeの使い分けを学んだところで、実務で最もやりがちなミスを1つ押さえておきましょう。
🍽️
「料理人が生きてるか?」(liveness)のチェックに
「食材倉庫が開いてるか?」を含めてしまうと、
倉庫が一時閉鎖しただけで、
全員クビ(Pod再起動)→ 倉庫は閉まったまま → 再雇用しても即クビ…
の無限ループになってしまう。
❌ NGパターン:livenessProbeにDB接続を含めた場合
✅ OKパターン:readinessProbeにDB接続を入れた場合
設計ルール:
| Probe | チェック内容 | DB障害時の振る舞い |
|---|---|---|
| livenessProbe | アプリ自体の生存のみ | Pod再起動しない(影響なし) |
| readinessProbe | DB/Redis含む完全な稼働状態 | Serviceから一時除外 → DB復旧で自動復帰 |
7-4. コンポーネント別の推奨設定
| コンポーネント | liveness | readiness |
|---|---|---|
| PostgreSQL |
pg_isready (exec) |
pg_isready (exec) |
| Redis |
redis-cli ping (exec) |
redis-cli ping (exec) |
| API |
/livez (httpGet) |
/readyz (httpGet) |
| nginx | tcpSocket :80 | httpGet /healthz
|
8. 新しい概念:resources(リソース制限)
8-1. requests と limits
resources:
requests: # 「最低これだけ欲しい」
memory: "64Mi"
cpu: "50m"
limits: # 「最大これまで使える」
memory: "128Mi"
cpu: "100m"
| フィールド | 意味 | 比喩 |
|---|---|---|
| requests | 最低限保証するリソース | 「最低でも2畳のスペースを確保」 |
| limits | 使える上限 | 「最大4畳まで」 |
8-2. 単位
| 種類 | 表記例 | 意味 |
|---|---|---|
| CPU | 100m |
0.1 vCPU(100ミリCPU) |
| CPU | 1 |
1 vCPU |
| メモリ | 128Mi |
128 MiB |
| メモリ | 1Gi |
1 GiB |
8-3. なぜ重要か
リソース制限がないと、
1つのPodがNode全体のリソースを食い尽くし、他のPodが動けなくなります。
🍽️
requests = 「このスタッフには最低1台の調理台を確保」。
limits = 「でも2台以上は使わないで」。
制限なし = 「1人が全調理台を独占 → 他のスタッフが料理できない」
9. Step 1:基盤(Namespace + ConfigMap + Secret)
📂 このStepで作成するファイル:
├── namespace.yaml ← 9-2
├── configmap.yaml ← 9-3
└── secret.yaml ← 9-4
9-1. 作業ディレクトリ
# ホームディレクトリで実行
cd ~
mkdir -p k8s-todo/{db,redis,api,web}
cd k8s-todo
9-2. Namespace
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: k8s-todo
9-3. ConfigMap
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: k8s-todo
data:
DATABASE_HOST: "db-0.db-headless"
DATABASE_PORT: "5432"
DATABASE_NAME: "k8s_todo"
REDIS_HOST: "redis-service"
REDIS_PORT: "6379"
APP_ENV: "production"
LOG_LEVEL: "info"
| キー | 値 | 用途 |
|---|---|---|
| DATABASE_HOST | db-0.db-headless | StatefulSet Pod への直接DNS名 |
| DATABASE_PORT | 5432 | PostgreSQL 標準ポート |
| DATABASE_NAME | k8s_todo | アプリが使うDB名 |
| REDIS_HOST | redis-service | Redis Service のDNS名 |
| REDIS_PORT | 6379 | Redis 標準ポート |
| APP_ENV | production | 実行環境の区分 |
| LOG_LEVEL | info | ログ出力レベル |
ConfigMap = 「アプリの設定ファイル」
環境ごとに異なる値(接続先ホスト名、ポート番号など)をYAMLで一元管理し、
Pod の環境変数に注入します。
機密情報は含めず、Secret と使い分けます。
9-4. Secret
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: k8s-todo
type: Opaque
stringData:
POSTGRES_USER: "todoapp"
POSTGRES_PASSWORD: "k8s-todo-secret-2024"
DATABASE_URL: "postgresql://todoapp:k8s-todo-secret-2024@db-0.db-headless:5432/k8s_todo"
| キー | 値 | 用途 |
|---|---|---|
| POSTGRES_USER | todoapp | DB接続ユーザー名 |
| POSTGRES_PASSWORD | k8s-todo-secret-2024 | DB接続パスワード |
| DATABASE_URL | postgresql://todoapp:...@db-0.db-headless:5432/k8s_todo | 完全な接続文字列(フレームワーク向け) |
ConfigMap vs Secret の使い分け
- ConfigMap → ホスト名やポートなど「見られても問題ない」設定値
- Secret → パスワードや接続文字列など「見られたら困る」機密情報
stringDataで平文指定すると、
Kubernetes が自動的に Base64 エンコードして保存します。
9-5. 適用と確認
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
# 確認
kubectl get configmap,secret -n k8s-todo
10. Step 2:DB層(StatefulSet + Headless Service)
📂 k8s-todo/
├── namespace.yaml ✅ Step 1で作成済み
├── configmap.yaml ✅ Step 1で作成済み
├── secret.yaml ✅ Step 1で作成済み
└── db/
├── headless-service.yaml ← 10-1 🆕
└── statefulset.yaml ← 10-2 🆕
10-1. Headless Service
# db/headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: db-headless
namespace: k8s-todo
spec:
clusterIP: None
selector:
app: db
ports:
- port: 5432
targetPort: 5432
protocol: TCP
| フィールド | 値 | 意味 |
|---|---|---|
| clusterIP | None | Headless Service(ロードバランスなし、Pod直接アクセス) |
| selector.app | db | app: db ラベルを持つPodにルーティング |
| port | 5432 | PostgreSQL 標準ポート |
Headless Service(clusterIP: None)とは?
通常のServiceは「仮想IP + ロードバランス」ですが、
Headless Serviceは仮想IPを持たず、DNS で Pod の実IPを直接返します。
StatefulSet と組み合わせると
db-0.db-headlessのようなPod固有のDNS名が使えるため、
データベースのように「どのPodに繋ぐか」を特定したい場合に使います。
10-2. StatefulSet
# db/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: k8s-todo
spec:
serviceName: db-headless
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:14
ports:
- containerPort: 5432
# Secretから環境変数
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-secret
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_NAME
# PVCマウント
volumeMounts:
- name: db-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
# ヘルスチェック
readinessProbe:
exec:
command: ["pg_isready", "-U", "todoapp"]
initialDelaySeconds: 5
periodSeconds: 10
# Podごとに専用PVCを自動作成
volumeClaimTemplates:
- metadata:
name: db-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 1Gi
| セクション | 設定値 | 意味 |
|---|---|---|
| serviceName | db-headless | 紐づくHeadless Service(Pod DNS名に使われる) |
| replicas | 1 | Podを1つ維持(DBは通常1台から開始) |
| image | postgres:14 | PostgreSQL 14 公式イメージ |
| env (secretKeyRef) | db-secret → POSTGRES_USER / PASSWORD | DB認証情報をSecretから注入 |
| env (configMapKeyRef) | app-config → DATABASE_NAME | DB名をConfigMapから注入 |
| volumeMounts | /var/lib/postgresql/data (subPath: pgdata) | データ保存先にPVCをマウント |
| readinessProbe | pg_isready -U todoapp | PostgreSQLが接続受付可能か確認 |
| volumeClaimTemplates | 1Gi, ReadWriteOnce, standard | Podごとに専用PVCを自動作成 |
StatefulSet vs Deployment の違い
- Deployment:Podは使い捨て、データは保持しない → Web/APIサーバー向け
- StatefulSet:Pod名が固定(db-0)、PVCも固定で再起動後もデータ保持 → DB向け
volumeClaimTemplatesは「Podが作られるたびに専用の永続ボリュームを自動作成する」テンプレートです。
10-3. 適用と確認
kubectl apply -f db/headless-service.yaml
kubectl apply -f db/statefulset.yaml
# Pod確認(db-0 が Running になるまで待つ)
kubectl get pods -n k8s-todo --watch
# PVC確認(自動作成されたか)
kubectl get pvc -n k8s-todo
期待される出力:
NAME READY STATUS AGE
db-0 1/1 Running 30s
NAME STATUS VOLUME CAPACITY STORAGECLASS
db-data-db-0 Bound pvc-xxx 1Gi standard
🔰 Pod名が db-0(連番)、PVC名が db-data-db-0(Pod名付き)— StatefulSetの特徴です。
10-4. DB接続テスト
# テストテーブル作成
kubectl exec -it db-0 -n k8s-todo -- psql -U todoapp -d k8s_todo -c "
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false
);
INSERT INTO todos (title) VALUES ('k8s学習Day5完了');
SELECT * FROM todos;
"
11. Step 3:Redis層(Deployment + Service)
📂 k8s-todo/
├── namespace.yaml ✅ Step 1
├── configmap.yaml ✅ Step 1
├── secret.yaml ✅ Step 1
├── db/
│ ├── headless-service.yaml ✅ Step 2
│ └── statefulset.yaml ✅ Step 2
└── redis/
├── service.yaml ← 11-1 🆕
└── deployment.yaml ← 11-2 🆕
11-1. Service
# redis/service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-service
namespace: k8s-todo
spec:
type: ClusterIP
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
protocol: TCP
| フィールド | 値 | 意味 |
|---|---|---|
| type | ClusterIP | クラスタ内部のみ公開(外部からは直接アクセスできない) |
| selector.app | redis | app: redis ラベルを持つPodにルーティング |
| port / targetPort | 6379 / 6379 | Redis標準ポート → Podの同じポートに転送 |
ClusterIP Service = 「クラスタ内専用の受付窓口」
他のPodからredis-service:6379というDNS名で接続できます。
外部公開が不要なバックエンド(Redis, DB)はClusterIPが適切です。
11-2. Deployment
# redis/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: k8s-todo
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 10
periodSeconds: 10
| セクション | 設定値 | 意味 |
|---|---|---|
| replicas | 1 | Podを1つ維持(キャッシュ用途なので単一構成) |
| image | redis:7-alpine | 軽量Alpine版のRedis 7 |
| resources.requests | 64Mi / 50m | 最低保証リソース(Nodeスケジューリング基準) |
| resources.limits | 128Mi / 100m | 上限(超過 → OOMKilled / スロットリング) |
| readinessProbe | redis-cli ping (3秒後, 5秒間隔) | 「受付OK?」→ 失敗時Serviceから除外 |
| livenessProbe | redis-cli ping (10秒後, 10秒間隔) | 「生きてる?」→ 失敗時コンテナ再起動 |
readinessProbe vs livenessProbe
- readinessProbe:「このPodにリクエストを送って大丈夫?」(失敗 → Serviceのルーティングから除外)
- livenessProbe:「このPodはまだ生きている?」(失敗 → コンテナを強制再起動)
readinessは「客を通すか」、
livenessは「店が開いているか」と考えるとイメージしやすいです。
11-3. 適用と確認
kubectl apply -f redis/service.yaml
kubectl apply -f redis/deployment.yaml
kubectl get pods -n k8s-todo
# Redis接続テスト
kubectl run redis-test \
--image=redis:7-alpine \
--rm -it \
--restart=Never \
-n k8s-todo \
-- redis-cli -h redis-service ping
# → PONG
12. Step 4:API層(Deployment + Service)
📂 k8s-todo/
├── namespace.yaml ✅ Step 1
├── configmap.yaml ✅ Step 1
├── secret.yaml ✅ Step 1
├── db/
│ ├── headless-service.yaml ✅ Step 2
│ └── statefulset.yaml ✅ Step 2
├── redis/
│ ├── service.yaml ✅ Step 3
│ └── deployment.yaml ✅ Step 3
└── api/
├── service.yaml ← 12-1 🆕
└── deployment.yaml ← 12-2 🆕
12-1. Service
# api/service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: k8s-todo
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 80
targetPort: 8080
protocol: TCP
| フィールド | 値 | 意味 |
|---|---|---|
| type | ClusterIP | クラスタ内部のみ公開 |
| selector.app | api | app: api ラベルを持つPodにルーティング |
| port | 80 | Service側ポート(呼び出し側は80でアクセス) |
| targetPort | 8080 | Pod側ポート(http-echoが8080でリッスン) |
port ≠ targetPort の場合
Serviceのport: 80とPodのtargetPort: 8080が異なるのは、
「外向きは標準ポート(80)で公開し、内部では別ポート(8080)で動作」というパターンです。
nginx設定のproxy_pass http://api-service:80/はService側ポート(80)を指定します。
12-2. Deployment
# api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: k8s-todo
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: hashicorp/http-echo:0.2.3
args:
- "-listen=:8080"
- "-text=k8s-todo API is running"
ports:
- containerPort: 8080
# ConfigMapから一括読み込み
envFrom:
- configMapRef:
name: app-config
# Secretから個別読み込み
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: DATABASE_URL
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
| セクション | 設定値 | 意味 |
|---|---|---|
| replicas | 2 | 2つ維持(1つ落ちてもサービス継続) |
| image | hashicorp/http-echo:0.2.3 | 模擬APIサーバー(固定テキストを返すだけ) |
| args | -listen=:8080, -text=... | リッスンポートと応答テキストを指定 |
| envFrom (configMapRef) | app-config | ConfigMapの全7キーを環境変数として一括注入 |
| env (secretKeyRef) | db-secret → DATABASE_URL | 接続文字列をSecretから個別注入 |
| resources | 64Mi-128Mi / 50m-100m | リソースの最低保証 〜 上限 |
| readinessProbe | HTTP GET / :8080 | HTTPレスポンスで受付可否を確認 |
| livenessProbe | HTTP GET / :8080 | HTTPレスポンスで生死を確認 |
envFrom vs env の使い分け
envFrom:ConfigMap/Secretの全キーを一括で環境変数に注入(キー名がそのまま変数名になる)env:個別キーを選んで注入(変数名を自由に指定可能)このDeploymentでは、ConfigMapは全キーが必要なので
envFrom、
SecretからはDATABASE_URLだけ必要なのでenvで取得しています。
12-3. 適用と確認
kubectl apply -f api/service.yaml
kubectl apply -f api/deployment.yaml
kubectl get pods -n k8s-todo
# API接続テスト
kubectl run api-test \
--image=busybox \
--rm -it \
--restart=Never \
-n k8s-todo \
-- wget -qO- http://api-service:80
# → k8s-todo API is running
13. Step 5:Web層(Deployment + Service + nginx設定)
📂 k8s-todo/
├── namespace.yaml ✅ Step 1
├── configmap.yaml ✅ Step 1
├── secret.yaml ✅ Step 1
├── db/
│ ├── headless-service.yaml ✅ Step 2
│ └── statefulset.yaml ✅ Step 2
├── redis/
│ ├── service.yaml ✅ Step 3
│ └── deployment.yaml ✅ Step 3
├── api/
│ ├── service.yaml ✅ Step 4
│ └── deployment.yaml ✅ Step 4
└── web/
├── configmap-nginx.yaml ← 13-1 🆕
├── configmap-html.yaml ← 13-2 🆕
├── service.yaml ← 13-3 🆕
└── deployment.yaml ← 13-4 🆕
13-1. nginx設定用ConfigMap
# web/configmap-nginx.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: k8s-todo
data:
default.conf: |
server {
listen 80;
server_name _;
# 静的ファイル配信
location / {
root /usr/share/nginx/html;
index index.html;
}
# APIへのリバースプロキシ
location /api/ {
proxy_pass http://api-service:80/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ヘルスチェック用
location /healthz {
return 200 'ok';
add_header Content-Type text/plain;
}
}
| locationブロック | パス | 動作 |
|---|---|---|
| 静的ファイル配信 | / |
/usr/share/nginx/html 配下のファイルを返す |
| APIリバースプロキシ | /api/ |
api-service:80 へ転送(末尾 / でパスを除去) |
| ヘルスチェック | /healthz |
常に 200 ok を返す(readinessProbe用) |
nginx の
proxy_pass末尾スラッシュの意味
proxy_pass http://api-service:80/;(末尾/あり)の場合、
/api/users→http://api-service:80/usersとパスの/api/部分が除去されます。
末尾/なしだと/api/usersがそのまま転送されるため、用途に応じて使い分けます。
13-2. トップページ用ConfigMap
# web/configmap-html.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: web-html
namespace: k8s-todo
data:
index.html: |
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>k8s-todo</title>
</head>
<body>
<h1>k8s-todo アプリケーション</h1>
<p>Kubernetes学習用タスク管理アプリ</p>
<ul>
<li>Web層: nginx (Deployment)</li>
<li>API層: http-echo (Deployment)</li>
<li>DB層: PostgreSQL (StatefulSet)</li>
<li>Cache層: Redis (Deployment)</li>
</ul>
<p><a href="/api/">API エンドポイント</a></p>
</body>
</html>
HTMLをConfigMapで管理するメリット
コンテナイメージを再ビルドせずに、kubectl applyだけでページ内容を更新できます。
学習環境や簡易的なコンテンツ配信に便利です。
本番環境では、CI/CDでイメージに組み込むか、外部CDNを使うのが一般的です。
13-3. Service
# web/service.yaml
apiVersion: v1
kind: Service
metadata:
name: web-service
namespace: k8s-todo
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 80
protocol: TCP
| フィールド | 値 | 意味 |
|---|---|---|
| type | ClusterIP | クラスタ内部のみ公開(外部公開はIngressが担当) |
| selector.app | web | app: web ラベルを持つPodにルーティング |
| port / targetPort | 80 / 80 | Serviceもnginxも80でリッスン(同一ポート) |
Ingress → Service → Pod のリクエスト経路
外部ユーザー → Ingress(ホスト名/パスで振り分け)→ web-service:80 → nginx Pod:80
ClusterIPなので外部からの直接アクセスはできず、必ずIngressを経由します。
13-4. Deployment
# web/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: k8s-todo
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
resources:
requests:
memory: "32Mi"
cpu: "25m"
limits:
memory: "64Mi"
cpu: "50m"
# ConfigMapをファイルとしてマウント
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: html-content
mountPath: /usr/share/nginx/html
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: nginx-conf
configMap:
name: nginx-config
- name: html-content
configMap:
name: web-html
13-5. 解説:web/deployment.yaml のConfigMapマウントの仕組み
ConfigMapをPod内のファイルとして使うには、
YAMLで2段階の設定が必要:
① volumes — 「何をマウントするか?」(ConfigMap名を指定)
② volumeMounts — 「コンテナのどこに置くか?」(mountPathを指定)
💡ポイント:
volumesとvolumeMountsはnameが一致することで紐づきます。
上の13-4のYAMLで、
volumes:セクション(Podレベル)と
volumeMounts:セクション(コンテナレベル)が対応しています。
Docker Composeとの対比:
| # | Docker Compose | k8s |
|---|---|---|
| 設定の置き場所 | ホストマシンのファイル | ConfigMap(クラスタ内リソース) |
| マウント方法 | volumes: ./nginx.conf:/etc/nginx/conf.d/ |
volumes + volumeMounts の2段階 |
| Node移動時 | ファイルがないNodeでは動かない | どのNodeでも同じ設定が使える |
13-6. 適用と確認
kubectl apply -f web/configmap-nginx.yaml
kubectl apply -f web/configmap-html.yaml
kubectl apply -f web/service.yaml
kubectl apply -f web/deployment.yaml
kubectl get pods -n k8s-todo
14. Step 6:Ingress(外部公開)
📂 k8s-todo/
├── namespace.yaml ✅ Step 1
├── configmap.yaml ✅ Step 1
├── secret.yaml ✅ Step 1
├── db/
│ ├── headless-service.yaml ✅ Step 2
│ └── statefulset.yaml ✅ Step 2
├── redis/
│ ├── service.yaml ✅ Step 3
│ └── deployment.yaml ✅ Step 3
├── api/
│ ├── service.yaml ✅ Step 4
│ └── deployment.yaml ✅ Step 4
├── web/
│ ├── configmap-nginx.yaml ✅ Step 5
│ ├── configmap-html.yaml ✅ Step 5
│ ├── service.yaml ✅ Step 5
│ └── deployment.yaml ✅ Step 5
└── ingress.yaml ← 14-1 🆕
14-1. Ingress YAML
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: todo-ingress
namespace: k8s-todo
spec:
ingressClassName: nginx
rules:
- host: todo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
主要フィールド解説:
| フィールド | 値 | 意味 |
|---|---|---|
ingressClassName |
nginx |
どのIngress Controllerが 処理するかを指定 |
host |
todo.local |
このホスト名でアクセスされた時だけ ルーティングする |
path / pathType
|
/ / Prefix
|
/ 以下の全リクエストを対象にする |
backend.service |
web-service:80 |
マッチしたリクエストの 転送先Service |
annotations: rewrite-target |
/ |
URLパスを書き換える (例: /foo → / に変換して転送) |
14-2. 適用と確認
# Ingress Controllerが有効か確認
minikube addons list | grep ingress
# 有効でなければ有効化
minikube addons enable ingress
# Ingress作成
kubectl apply -f ingress.yaml
# 確認
kubectl get ingress -n k8s-todo
todo.local は本物のドメインではないので、
ブラウザは「このサイトどこにあるの?」と迷子になります。
PCの「ローカル電話帳」にあたる
/etc/hosts に
「todo.local = minikubeのIPアドレス」と書いてあげることで、
ブラウザがアクセスできるようになります。
# todo.local → minikubeのIP に名前解決できるようにする
echo "$(minikube ip) todo.local" | sudo tee -a /etc/hosts
14-3. L7ルーティングの設計判断
YAMLを見て
「/api へのルーティングはIngressに書かない?」と思いましたが、
今回は /api のルーティングを
Ingressではなくnginx(Web Pod)が担当しています。
① 今回の構成:nginxが /api を振り分ける
Ingressは「全部nginxに渡す」だけ。
/apiの振り分けはnginx内のproxy_passが担当。
② もう1つの方式:Ingressが /api を直接振り分ける
Ingressがパスごとに振り分け先を変える。nginxは静的ファイル配信だけ。
使い分けの目安:
| 方式 | L7ルーティングの場所 | メリット | 向いているケース |
|---|---|---|---|
| ①nginx内で振り分け(今回) | nginx Pod内 (proxy_pass) |
nginx設定で 柔軟に制御 |
フロントエンドが nginxの場合 |
| ②Ingressで振り分け | Ingress YAML (複数path) |
YAML1つで管理 シンプル |
マイクロサービスが 多い場合 |
15. 全体の通信フローと動作確認
15-1. 通信フロー
15-1-1. 全体シーケンス
⚠️ 注意: シーケンス図中のAPI→Redis・API→DBの通信は本番構成を想定した流れです。模擬用の
http-echoは固定文字列を返すのみで、実際にはRedis・DBに接続しません。
15-1-2. 分割版:3つのフェーズで理解する
Phase 1:静的ページ取得(GET /)
ユーザーのリクエストが
Ingressを経由してnginxに届き、HTMLがそのまま返ります。
Phase 2:APIリクエストの転送(GET /api/)
nginxが
/api/を検知してリバースプロキシとしてAPI Podに転送。
レスポンスは来た道を逆に辿って返ります。
Phase 3:APIのバックエンド処理(Redis + DB)
API PodがまずRedisにキャッシュを確認し、なければDBに問い合わせます。
※ 本模擬PJではhttp-echoを使用しているため、この通信は実際には発生しません。本番アプリケーションではこの流れになります。
15-2. 外部アクセステスト
15-1で学んだ通信フローを、実際にリクエストを送って確認します。
① 静的ページ取得(Phase 1 に対応)
curl http://todo.local/
期待される出力:
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>k8s-todo</title></head>
<body><h1>k8s-todo アプリケーション</h1>...
② APIリクエスト(Phase 2・3 に対応)
curl http://todo.local/api/
期待される出力:
k8s-todo API is running
③ ヘルスチェック(nginx の /healthz エンドポイント)
curl http://todo.local/healthz
期待される出力:
ok
⚠️うまくいかない場合のチェックリスト:
症状 確認すること Could not resolve host/etc/hostsにtodo.localのエントリがあるか確認Connection refusedminikube addons enable ingressでIngress Controllerが有効か確認404 Not Foundkubectl get ingress -n k8s-todoでIngressリソースが作成されているか確認502 Bad Gatewaykubectl get pods -n k8s-todoで全Podが Running / Ready か確認
15-3. 全リソース一覧
kubectl get all,configmap,secret,pvc,ingress -n k8s-todo
| リソース | 名前 | 学んだ回 |
|---|---|---|
| Namespace | k8s-todo | 第5回 |
| ConfigMap | app-config | 第4回 |
| ConfigMap | nginx-config | 第4回 |
| ConfigMap | web-html | 第4回 |
| Secret | db-secret | 第4回 |
| StatefulSet | db (×1) | 第4回 |
| Headless Service | db-headless | 第4回 |
| PVC | db-data-db-0 | 第4回 |
| Deployment | redis (×1) | 第2回 |
| Service | redis-service | 第3回 |
| Deployment | api (×2) | 第2回 |
| Service | api-service | 第3回 |
| Deployment | web (×2) | 第2回 |
| Service | web-service | 第3回 |
| Ingress | todo-ingress | 第3回 |
16. お片付けとNamespace削除の注意点
16-1. Namespace削除で全リソース一括削除
kubectl delete namespace k8s-todo
# 確認
kubectl get all -n k8s-todo
# → "No resources found in k8s-todo namespace."
16-2. ⚠️ Namespace削除とPVのReclaimPolicy
16-2-1. ReclaimPolicyとは?
ReclaimPolicyは
「PVCが削除されたとき、PV(実データ)をどうするか?」を決めるルールです。
比喩:賃貸の退去ルール
ルール 賃貸での例 PVでの動作 Delete 退去時に家具も全撤去(原状回復) PV + データを自動削除 Retain 家具を残して退去(大家が後で判断) PVは残る(手動で対応)
16-2-2. このプロジェクトへの影響
16-1 で kubectl delete namespace k8s-todo を実行すると、
Namespace内の
PVC db-data-db-0(PostgreSQLのデータ)も一緒に削除されます。
このとき、
ReclaimPolicyによってデータの運命が分かれます。
minikube / AWS EKS / GCP GKE いずれもデフォルトは Delete です。
つまり、
何も設定を変えずにNamespaceを削除すると データは消えます。
16-2-3. 開発 vs 本番の使い分け
| 環境 | 推奨ReclaimPolicy | デフォルト | 理由 |
|---|---|---|---|
| 開発・学習 | Delete | そのまま | 使い捨てでOK。今回はこちら |
| 本番 | Retain | 要変更 | データ保護。手動確認後に削除 |
⚠️ 本番では必ず Retain + バックアップ。
今回は学習用なのでDeleteで問題ありません。
16-2-4. Retain時のPV再利用手順
Retainを設定した場合、
PVC削除後のPVは Released(使用済み)状態になります。
再利用するには、以下の手順でAvailableに戻します。
# Released状態のPVをAvailableに戻す
kubectl patch pv <PV名> \
-p '{"spec":{"claimRef":null}}'
17. まとめ
17-1. 本記事で学んだこと
| # | 学んだこと | キーポイント | 該当セクション |
|---|---|---|---|
| ① | 4層構成の設計と構築 | 依存関係の下から積み上げる(DB→Redis→API→Web→Ingress) | 5, 9〜14 |
| ② | 第1〜4回の全リソースの統合 | 15個のリソースが連携して1つのアプリを構成 | 5-1, 15-3 |
| ③ | Namespace | プロジェクト分離。一括削除のメリットと注意点 | 6, 16 |
| ④ | Probe(ヘルスチェック) | readiness/liveness/startupの使い分け。livenessにDB接続を入れない | 7 |
| ⑤ | resources(リソース制限) | requests(最低保証)とlimits(上限)でリソース管理 | 8 |
| ⑥ | ConfigMapファイルマウント | nginx.confとindex.htmlをConfigMapからマウント | 13-5 |
| ⑦ | ReclaimPolicyとNamespace削除 | Delete=データ消失、Retain=データ残る(手動対応必要) | 16-2 |
17-2. 次回予告
第6回:トラブルシュートと運用 — エラー対応とログ確認の実践
本記事で構築した環境を使って、意図的にエラーを発生させて対処する実践的なトラブルシュート演習を行います。
- 頻出エラーパターン — CrashLoopBackOff / ImagePullBackOff / Pending 等の原因特定と解決
-
ログ確認 —
kubectl logs/kubectl describeによるデバッグ手法 - 実践演習 — 設定ミスを仕込んで、自力で原因を特定→修正する体験
(つづく)