0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetes⑤(全7回)|minikube|模擬プロジェクトで全概念を組み合わせる — Web+API+DB+Redis構成

0
Last updated at Posted at 2026-02-18

📁GitHubにコード公開GitHub: kubernetes_minikube

シリーズ記事一覧

📑 目次

  1. この記事について
  2. この記事のゴール
  3. 前提条件
  4. プロジェクト概要:k8s-todo
  5. アーキテクチャ設計
  6. 新しい概念:Namespace
  7. 新しい概念:Probe(ヘルスチェック)
  8. 新しい概念:resources(リソース制限)
  9. Step 1:基盤(Namespace + ConfigMap + Secret)
  10. Step 2:DB層(StatefulSet + Headless Service)
  11. Step 3:Redis層(Deployment + Service)
  12. Step 4:API層(Deployment + Service)
  13. Step 5:Web層(Deployment + Service + nginx設定)
  14. Step 6:Ingress(外部公開)
  15. 全体の通信フローと動作確認
  16. お片付けとNamespace削除の注意点
  17. まとめ

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/usershttp://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を指定)

💡ポイント:
volumesvolumeMountsname が一致することで紐づきます。
上の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/hoststodo.local のエントリがあるか確認
Connection refused minikube addons enable ingress でIngress Controllerが有効か確認
404 Not Found kubectl get ingress -n k8s-todo でIngressリソースが作成されているか確認
502 Bad Gateway kubectl 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 によるデバッグ手法
  • 実践演習 — 設定ミスを仕込んで、自力で原因を特定→修正する体験

(つづく)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?