5.1 はじめに
5.1.1 前回の振り返り — 基盤が整った状態
前回(第4回)で、アプリケーションを載せるための「箱」を整備しました。現在のクラスタの状態を確認しておきましょう。
[クラスタ基盤]
kindクラスタ(CP 1 + Worker 3)— 全ノードReady
Calico CNI — 稼働中
Metrics Server — 稼働中
Gateway APIコントローラー(Envoy Gateway)— 稼働中
[Namespace + リソース管理]
app — ResourceQuota + LimitRange 適用済み
db — ResourceQuota + LimitRange 適用済み
monitoring — ResourceQuota + LimitRange 適用済み
[RBAC]
developer / operator ServiceAccount + Role + RoleBinding — 各Namespace適用済み
[アプリケーション]
なし(白紙状態)
Namespace、ResourceQuota、LimitRange、RBACが設計書通りに構成されています。しかし、アプリケーションはまだ何もデプロイされていません。基盤は整ったが、中身は空——これが今回の出発点です。
5.1.2 本回の問題提起 — 「どの順番で適用するのか」
第3回(詳細設計)で、TaskBoardの全マニフェストを作成しました。Deployment、StatefulSet、Service、HPA、PDB、Secret、ConfigMap、Job、CronJob、DaemonSet——合計十数個のマニフェストが手元にあります。
では、これらをどの順番でクラスタに適用するのか。
応用編では、学習テーマに沿って1つずつリソースを追加していきました。「第3回でStatefulSetを追加」「第4回でJobとCronJobを追加」という具合に、毎回の学習目的が適用順序を決めていました。
実践編の構築は違います。設計書が先にあり、依存関係に基づいた計画的な順序でマニフェストを適用します。「SecretがStatefulSetより先に必要」「データベースがアプリケーションより先に必要」——こうした依存関係を整理し、各レイヤーで検証してから次に進む。これが本番の構築プロセスです。
5.1.3 本回のゴールと成果物
本回のゴールは、設計書のマニフェストを依存順序に従ってクラスタに適用し、全コンポーネントが内部的に正常稼働している状態を作ることです。
成果物は以下の2つです。
- 内部稼働中のTaskBoard: Secret/ConfigMap、MySQL(StatefulSet)、TaskBoard API(Deployment)、Nginx(Deployment)、DB初期化Job、DBバックアップCronJob、ログ収集DaemonSet、HPA、PDBが全て動作している状態
- 構築手順書: 本回で実行した手順そのものが、依存関係を考慮した構築手順書の実例になります
Gateway APIリソース(Gateway、HTTPRoute)とNetworkPolicyは本回では適用しません。まず全コンポーネントが内部で正常に動作することを確認し、外部公開と通信制御は第6回で追加します。この段階的なアプローチの理由は5.2節で説明します。
5.2 VMの構築手順とK8sのデプロイ順序
5.2.1 VMの世界での構築手順 — DB→AP→Webの依存順序
VMwareの世界でWebシステムを構築する際の手順書を思い出してください。3層構成のシステムであれば、構築順序は決まっていました。
- 第1段階: データベースサーバー — MySQLをインストールし、スキーマを作成し、接続テストを行う。「DBが動いている」ことを確認してから次へ
- 第2段階: アプリケーションサーバー — Payara(あるいはTomcat)をインストールし、WARをデプロイし、DBへの接続を確認する。「APがDBに接続できる」ことを確認してから次へ
- 第3段階: Webサーバー — Nginxをインストールし、リバースプロキシを設定し、APへの転送を確認する
- 第4段階: ファイアウォール設定 — 全サーバー間の通信が確認できてから、ファイアウォールルールを適用する
各段階の間には必ず「完了確認」がありました。DBサーバーの構築手順書には「mysql -u root -p でログインできること」という確認項目があり、APサーバーの手順書には「ヘルスチェックURLにアクセスして200が返ること」という確認項目がありました。確認せずに次に進むと、問題の切り分けが困難になります。
K8sでの構築も同じです。依存関係を考慮した順序で適用し、各レイヤーで検証してから次に進む。ファイアウォール(NetworkPolicy)は最後に設定する。このリズムはVMの構築手順書と変わりません。
5.2.2 K8sのマニフェスト適用順序 — 依存関係グラフ
第4回の4.2.3節で設計した全体の構築順序のうち、本回(7〜15番)の適用順序を依存関係に基づいて整理します。
Step 1: Secret / ConfigMap
└→ 他のリソースから参照される「前提条件」
Step 2: MySQL StatefulSet + Headless Service
└→ アプリケーションが依存するデータストア
└→ SecretをenvFromで参照 → Step 1に依存
Step 3: DB初期化Job
└→ MySQLが起動していないと実行できない → Step 2に依存
└→ SecretをenvFrom/secretKeyRefで参照 → Step 1に依存
Step 4: TaskBoard API Deployment + Service / Nginx Deployment + Service
└→ DBが稼働してから初めてデプロイ可能(APIはDB接続が必要)
└→ SecretをsecretKeyRefで参照 → Step 1に依存
└→ ConfigMapをvolumeMountで参照 → Step 1に依存
Step 5: 補助ワークロード(CronJob / DaemonSet)
└→ CronJobはDBに接続 → Step 2に依存
└→ DaemonSetは独立だが、コアが動いてから追加が安全
Step 6: スケーリング・可用性(HPA / PDB)
└→ 対象のDeploymentが存在していること → Step 4に依存
└→ HPAはMetrics Serverからメトリクスを取得
Step 7: 内部疎通テスト
└→ 全コンポーネントが稼働していること
この順序を崩すと何が起きるか、いくつかの例を挙げます。
- SecretよりStatefulSetを先に適用した場合: MySQLのPodがSecretを参照できず、
CreateContainerConfigErrorで起動に失敗します - MySQLよりTaskBoard APIを先にデプロイした場合: APIがDB接続に失敗し、readinessProbe(DB接続確認を含む)が通らず、ServiceのEndpointに登録されません
- DeploymentよりHPAを先に適用した場合:
ScaleTargetRefが見つからず、HPAがUnable to find targetのエラーを出し続けます
なお、本回ではNetworkPolicyとGateway APIリソース(Gateway、HTTPRoute)を適用しません。これには明確な理由があります。VMの構築でも「まずサーバー間通信を確認してからファイアウォールを設定する」のが一般的です。NetworkPolicyが未適用の状態で全Pod間の疎通を確認しておけば、第6回でNetworkPolicyを適用した後に通信が遮断された場合、「NetworkPolicyが原因」と即座に切り分けられます。段階的に構築し、各段階で動作を確認する——これがトラブル切り分けを容易にする基本原則です。
5.2.3 適用前の安全確認 — dry-run と diff
応用編ではkubectl apply -fを直接実行していました。実践編では適用前に2つの安全確認を行います。
kubectl apply –dry-run=server は、マニフェストをAPIサーバーに送信してバリデーションを実行しますが、実際にはリソースを作成しません。--dry-run=clientがクライアント側でYAMLの文法チェックのみ行うのに対し、--dry-run=serverはAPIサーバーのwebhookやadmission controllerも実行されるため、より正確な事前検証ができます。
kubectl diff は、現在のクラスタ状態と適用予定のマニフェストの差分を表示します。新規作成時は全フィールドが差分として表示されますが、既存リソースの更新時に威力を発揮します。「意図しないフィールドが変更されていないか」を確認できます。
この2つのコマンドは、本番環境でマニフェストを適用する前の標準的な確認手順です。本回の各ステップで実際に使用します。
5.3 コンテナイメージを準備する
マニフェストを適用する前に、コンテナイメージを準備します。応用編ではTaskBoard APIのイメージを段階的にビルドしていきました。第1回でインメモリ版(1.0.0)、第3回でMySQL接続版(2.0.0)、第8回でカスタムHealthCheck追加版(3.0.0)と、学習テーマに合わせて段階的にファイルを追加・差し替えていきました。
実践編では、完成版のソースコードを一括で配置してビルドします。応用編は「学習のためのステップバイステップ」、実践編は「本番のように一括構築」——このプロセスの違いを意識してください。
5.3.1 TaskBoard APIのDockerイメージをビルドする
まず、作業ディレクトリを準備し、TaskBoard APIのソースコード一式を配置します。以下のファイルは、応用編で段階的に作成・更新してきたものの完成版です。
| ファイル | 応用編での初出 | 役割 |
|---|---|---|
| pom.xml | 応用編 第1回 | Mavenビルド定義(Jakarta EE 11 / MicroProfile 6.1 依存) |
| Dockerfile | 応用編 第1回 | multi-stage build定義 |
| TaskBoardApplication.java | 応用編 第1回 | JAX-RSアプリケーション定義(@ApplicationPath) |
| TaskResource.java | 応用編 第1回 | REST APIエンドポイント(/api/tasks) |
| Task.java | 応用編 第1回→第3回でJPAアノテーション追加 | JPAエンティティ |
| TaskService.java | 応用編 第1回→第3回で差し替え | MySQL接続版(JPA) |
| DataSourceConfig.java | 応用編 第3回 | @DataSourceDefinitionによるJDBCデータソース定義 |
| persistence.xml | 応用編 第3回 | MySQL接続設定(JPA) |
| HealthCheckLive.java | 応用編 第8回 | カスタムLivenessチェック |
| HealthCheckReady.java | 応用編 第8回 | カスタムReadinessチェック(DB接続確認を含む) |
| beans.xml | 応用編 第8回 | CDIビーン発見の有効化(bean-discovery-mode=”all”) |
応用編の作業ディレクトリ(~/k8s-applied/taskboard-api/)に完成版のソースコードが残っています。これをそのままコピーして使います。応用編は「学習のためのステップバイステップ」でしたが、実践編では既にあるものを活用するのが本番のアプローチです。
[Execution User: developer]
# 実践編の作業ディレクトリを作成し、応用編のソースコードをコピー
mkdir -p ~/k8s-production/manifests
rm -rf ~/k8s-production/taskboard-api
cp -r ~/k8s-applied/taskboard-api ~/k8s-production/
応用編第8回では学習ステップに合わせてイメージタグを3.0.0としていましたが、実践編では完成版を2.0.0として一括ビルドします。HealthCheckLive.java内のバージョン文字列とpom.xmlのバージョンを修正します。
[Execution User: developer]
# HealthCheckLive.javaのバージョン文字列を 3.0.0 → 2.0.0 に変更
sed -i 's/"version", "3.0.0"/"version", "2.0.0"/' \
~/k8s-production/taskboard-api/src/main/java/com/taskboard/HealthCheckLive.java
# pom.xmlのプロジェクトバージョンを 3.0.0 → 2.0.0 に変更
sed -i '0,/<version>3.0.0<\/version>/s/<version>3.0.0<\/version>/<version>2.0.0<\/version>/' \
~/k8s-production/taskboard-api/pom.xml
ディレクトリ構成を確認します。
[Execution User: developer]
find ~/k8s-production/taskboard-api -type f | sort
/home/developer/k8s-production/taskboard-api/Dockerfile
/home/developer/k8s-production/taskboard-api/pom.xml
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/DataSourceConfig.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/HealthCheckLive.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/HealthCheckReady.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/Task.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/TaskBoardApplication.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/TaskResource.java
/home/developer/k8s-production/taskboard-api/src/main/java/com/taskboard/TaskService.java
/home/developer/k8s-production/taskboard-api/src/main/resources/META-INF/persistence.xml
/home/developer/k8s-production/taskboard-api/src/main/webapp/WEB-INF/beans.xml
変更が正しく反映されたことを確認しておきます。
[Execution User: developer]
grep 'withData' ~/k8s-production/taskboard-api/src/main/java/com/taskboard/HealthCheckLive.java
.withData("version", "2.0.0")
応用編では第1回から第8回にかけて段階的に追加してきた11ファイルが、ここでは最初から全て揃っています。Dockerイメージをビルドします。
[Execution User: developer]
cd ~/k8s-production/taskboard-api
docker build --no-cache -t taskboard-api:2.0.0 .
multi-stage buildにより、Mavenビルドステージでソースコードがコンパイルされ、実行ステージでpayara/micro:7.2026.1ベースのイメージが作成されます。ホストにJDKやMavenをインストールする必要はありません。--no-cacheを付けているのは、応用編でビルドした際のDockerキャッシュが残っていても確実に新しいソースが反映されるようにするためです。ビルドの最終行にnaming to docker.io/library/taskboard-api:2.0.0と表示されれば成功です。
イメージが作成されたことを確認します。
[Execution User: developer]
docker images taskboard-api
REPOSITORY TAG IMAGE ID CREATED SIZE
taskboard-api 2.0.0 a1b2c3d4e5f6 10 seconds ago 250MB
5.3.2 Nginxのカスタムイメージを準備する
Nginxは標準のnginx:1.27イメージをそのまま使用します。応用編第7回で非root化した際、カスタムDockerイメージを作成するのではなく、ConfigMapで非root対応版のnginx.confをマウントする方式を採用しました。第3回の詳細設計でも同じ方式を設計しています。
このため、Nginxについては別途のイメージビルドは不要です。ただし、Docker Hubのnginx:1.27はマルチアーキテクチャ(amd64/arm64等)のマニフェストリスト形式で配布されており、この形式のままkind load docker-imageを実行すると、containerdのインポート処理でレイヤー不整合エラーが発生する場合があります。docker buildでFROM --platform=linux/amd64を指定し、シングルプラットフォームのイメージとして取得します。
[Execution User: developer]
# kindにロードするため、シングルプラットフォームのイメージとして取得する
# Docker Hubのnginx:1.27はマルチアーキテクチャイメージのため、そのままkind loadすると
# containerdのインポート処理でレイヤー不整合エラーが発生する場合がある
# docker buildでFROM --platformを指定し、確実にamd64のみのイメージを作成する
# --provenance=falseでBuildKitのattestation生成を無効化し、シングルマニフェスト形式にする
docker rmi nginx:1.27 2>/dev/null || true
docker build --no-cache --provenance=false -t nginx:1.27 - <<'EOF'
FROM --platform=linux/amd64 nginx:1.27
EOF
5.3.3 kindクラスタにイメージを投入する
ビルドしたTaskBoard APIイメージと、Nginxイメージをkindクラスタに投入します。kindクラスタのNodeはDockerコンテナであり、ホスト上のDockerイメージを直接参照できません。kind load docker-imageでイメージを各Nodeに転送します。
[Execution User: developer]
# TaskBoard APIイメージをkindクラスタに投入
kind load docker-image taskboard-api:2.0.0 --name k8s-applied
# Nginxイメージをkindクラスタに投入
kind load docker-image nginx:1.27 --name k8s-applied
Image: "taskboard-api:2.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker", loading...
Image: "taskboard-api:2.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker2", loading...
Image: "taskboard-api:2.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker3", loading...
Image: "taskboard-api:2.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-control-plane", loading...
Image: "nginx:1.27" with ID "sha256:e5f6a7b8..." not yet present on node "k8s-applied-worker", loading...
Image: "nginx:1.27" with ID "sha256:e5f6a7b8..." not yet present on node "k8s-applied-worker2", loading...
Image: "nginx:1.27" with ID "sha256:e5f6a7b8..." not yet present on node "k8s-applied-worker3", loading...
Image: "nginx:1.27" with ID "sha256:e5f6a7b8..." not yet present on node "k8s-applied-control-plane", loading...
全4ノード(CP 1 + Worker 3)に両イメージが転送されました。これでコンテナイメージの準備は完了です。
5.4 Step 1 — Secret / ConfigMapを適用する
最初に適用するのは、他のリソースから参照される「前提条件」であるSecretとConfigMapです。VMの構築で言えば、ミドルウェアをインストールする前に設定ファイルとパスワードを準備する段階に相当します。
5.4.1 MySQL Secretを適用する
まず、dry-runで事前検証を行います。実践編での最初のマニフェスト適用なので、--dry-run=serverの使い方を確認しておきましょう。
マニフェストの内容は第3回(詳細設計)の3.6.2節で設計した通りです。db NamespaceとApp Namespaceの両方にSecretを作成します。
第3回(詳細設計)の3.6.2節で設計したSecretをマニフェストファイルとして作成します。
[Execution User: developer]
# db Namespace用Secret(MySQL全認証情報)
cat <<'EOF' > ~/k8s-production/manifests/mysql-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
namespace: db
labels:
app: taskboard
component: db
type: Opaque
stringData:
MYSQL_ROOT_PASSWORD: "taskboard-root-pass"
MYSQL_DATABASE: "taskboard"
MYSQL_USER: "taskboard"
MYSQL_PASSWORD: "taskboard-pass"
EOF
# app Namespace用Secret(TaskBoard APIが参照する認証情報のみ)
cat <<'EOF' > ~/k8s-production/manifests/mysql-secret-app.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
namespace: app
labels:
app: taskboard
component: api
type: Opaque
stringData:
MYSQL_USER: "taskboard"
MYSQL_PASSWORD: "taskboard-pass"
EOF
db NamespaceのSecretには4キー(ROOT_PASSWORD、DATABASE、USER、PASSWORD)、app NamespaceのSecretにはTaskBoard APIが参照する2キー(USER、PASSWORD)のみを格納しています。Secretはクロスnamespace参照ができないため、両方のNamespaceに作成する必要があります(応用編第3回で学びました)。
[Execution User: developer]
# dry-run(サーバーサイド検証)
kubectl apply -f ~/k8s-production/manifests/mysql-secret.yaml --dry-run=server
secret/mysql-secret created (server dry run)
(server dry run)と表示されています。APIサーバーのバリデーションを通過しましたが、実際にはリソースは作成されていません。Namespaceやフィールドのスキーマに問題がないことが事前に確認できました。問題がなければ実際に適用します。
[Execution User: developer]
# db Namespace用Secret
kubectl apply -f ~/k8s-production/manifests/mysql-secret.yaml
# app Namespace用Secret(TaskBoard APIが参照する認証情報のみ)
kubectl apply -f ~/k8s-production/manifests/mysql-secret-app.yaml
secret/mysql-secret created
secret/mysql-secret created
5.4.2 Nginx ConfigMapを適用する
応用編第7回で作成した非root対応版のnginx.confをConfigMapとして適用します。listen 8080、PID/一時ファイル/ログを/tmp配下に配置する設定です。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/manifests/nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: app
labels:
app: taskboard
component: frontend
data:
nginx.conf: |
worker_processes auto;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /tmp/access.log main;
error_log /tmp/error.log;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
EOF
[Execution User: developer]
# dry-runで事前検証
kubectl apply -f ~/k8s-production/manifests/nginx-configmap.yaml --dry-run=server
configmap/nginx-config created (server dry run)
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/nginx-configmap.yaml
configmap/nginx-config created
5.4.3 適用結果を確認する
Step 1の検証です。Secret/ConfigMapが正しいNamespaceに作成されたことを確認します。
[Execution User: developer]
# db Namespaceの確認
kubectl get secret -n db
# app Namespaceの確認
kubectl get secret,configmap -n app
NAME TYPE DATA AGE
mysql-secret Opaque 4 15s
NAME TYPE DATA AGE
mysql-secret Opaque 2 12s
NAME DATA AGE
configmap/nginx-config 1 8s
db NamespaceのSecretは4キー(ROOT_PASSWORD、DATABASE、USER、PASSWORD)、app NamespaceのSecretは2キー(USER、PASSWORD)、ConfigMapは1キー(nginx.conf)です。設計書通りの構成であることを確認できました。Step 1完了です。
5.5 Step 2 — データベース層を構築する
VMの構築順序でも最初に構築するのはDBサーバーです。K8sでも同じく、アプリケーション層が依存するデータベース層を先に構築します。
5.5.1 MySQL StatefulSetをデプロイする
MySQL StatefulSetのマニフェストは第3回で設計したものです。StatefulSetとHeadless Serviceを1つのファイルにまとめて適用します。
まずkubectl diffで差分を確認してみましょう。新規作成時は全フィールドが差分として表示されますが、リソースの内容を適用前に確認する習慣をつけておくことが重要です。
MySQL StatefulSetとHeadless Serviceのマニフェストを作成します。第3回で設計した内容です。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/manifests/mysql-statefulset.yaml
# Headless Service(StatefulSetと同一ファイルにまとめる)
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: db
labels:
app: taskboard
component: db
spec:
clusterIP: None
selector:
app: taskboard
component: db
ports:
- port: 3306
targetPort: 3306
name: mysql
---
# MySQL StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: db
labels:
app: taskboard
component: db
spec:
serviceName: mysql-headless
replicas: 1
selector:
matchLabels:
app: taskboard
component: db
template:
metadata:
labels:
app: taskboard
component: db
spec:
containers:
- name: mysql
image: mysql:8.0
envFrom:
- secretRef:
name: mysql-secret
ports:
- containerPort: 3306
name: mysql
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 3306
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
securityContext:
runAsNonRoot: true
runAsUser: 999
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
- name: tmp
mountPath: /tmp
- name: run-mysqld
mountPath: /var/run/mysqld
volumes:
- name: tmp
emptyDir: {}
- name: run-mysqld
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
[Execution User: developer]
# 適用前に差分を確認
kubectl diff -f ~/k8s-production/manifests/mysql-statefulset.yaml
新規作成のため、マニフェストの全内容が差分(+行)として表示されます。内容に問題がなければ適用します。
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/mysql-statefulset.yaml
service/mysql-headless created
statefulset.apps/mysql created
5.5.2 kubectl waitでPod起動を待機する
応用編ではPodの起動をkubectl get pods -n dbで手動確認していました。実践編ではkubectl waitを使って起動完了をスクリプト的に待機します。構築手順書に組み込めるコマンドであり、CI/CDパイプラインでも同様に使用されます。
[Execution User: developer]
# mysql-0がReady状態になるまで待機(最大120秒)
kubectl wait --for=condition=ready pod/mysql-0 -n db --timeout=120s
pod/mysql-0 condition met
condition metが表示されれば、MySQLのPodが起動し、readinessProbe(tcpSocket port 3306)が通過しています。MySQLの初回起動にはデータベースの初期化処理が含まれるため、20〜40秒ほどかかることがあります。タイムアウトを120秒に設定しているのはそのためです。
PVCの自動作成も確認しておきます。StatefulSetのvolumeClaimTemplatesにより、PVCが自動的に作成されます。
[Execution User: developer]
kubectl get pvc -n db
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-data-mysql-0 Bound pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890 1Gi RWO standard 45s
PVCのステータスがBoundであり、1GiのPersistentVolumeが割り当てられています。
5.5.3 MySQL接続テストで動作を検証する
PodがRunningでも、MySQLプロセスが正常に動作しているとは限りません。実際に接続して確認します。
[Execution User: developer]
# mysql-0に直接接続して動作確認
kubectl exec -it mysql-0 -n db -- mysql -u taskboard -ptaskboard-pass taskboard -e "SELECT 1 AS test;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+------+
| test |
+------+
| 1 |
+------+
SELECT 1が正常に返りました。MySQLが起動し、Secretで定義した認証情報(ユーザー: taskboard、データベース: taskboard)で接続できることを確認できました。Step 2完了です。
5.6 Step 3 — DB初期化Jobを実行する
MySQLが稼働したので、初期データを投入するJobを実行します。テーブル構造(スキーマ)はTaskBoard APIのJPAが初回起動時に自動生成しますが、初期データの投入はJobが担当します。応用編第4回で設計した役割分担です。
ただし、JPAによるテーブル自動生成はTaskBoard APIの初回起動時に行われるため、本来はAPI起動後にJobを実行する順序が正しいように見えます。ここでは「DB初期化Jobは、API起動後にテーブルが存在する状態で実行する」という前提で進めます。実際には、JobのSQLスクリプト内でMySQLの起動待機を行い、テーブルが存在しない場合はJobが再試行される設計になっています(backoffLimit: 3)。
適用順序の観点では、DB初期化Jobの実行タイミングには2つの選択肢があります。
- 選択肢A: MySQL起動直後にJobを実行する(テーブルがなければJob失敗→API起動後に再実行)
- 選択肢B: TaskBoard API起動後にJobを実行する(JPAがテーブルを作成した後に初期データ投入)
本回ではStep 4(API起動)の後にDB初期化を実行する方が確実ですが、構築手順書としての明確さを優先し、データベース層の構築としてここでJobをapplyしておきます。Job内の待機ロジックとbackoffLimitにより、テーブルが存在しない場合は再試行されます。TaskBoard APIが起動してJPAがテーブルを作成した後に、Jobが成功する流れになります。
5.6.1 DB初期化Jobを適用する
DB初期化Jobのマニフェストを作成します。応用編第4回で設計した内容と同一です。
[Execution User: developer]
cat <<'OUTER' > ~/k8s-production/manifests/db-init-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: db-init
namespace: db
labels:
app: taskboard
component: db-init
spec:
backoffLimit: 3
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: taskboard
component: db-init
spec:
restartPolicy: Never
containers:
- name: db-init
image: mysql:8.0
command:
- sh
- -c
- |
echo "Waiting for MySQL to be ready..."
for i in $(seq 1 30); do
if mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "SELECT 1" > /dev/null 2>&1; then
echo "MySQL is ready."
break
fi
echo "Attempt $i: MySQL not ready, retrying in 2s..."
sleep 2
done
echo "Inserting initial data..."
mysql --default-character-set=utf8mb4 -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" <<SQL
CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
status VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO tasks (title, status)
SELECT 'プロジェクト計画の作成', 'open'
FROM dual WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE title = 'プロジェクト計画の作成');
INSERT INTO tasks (title, status)
SELECT 'サーバー環境の構築', 'open'
FROM dual WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE title = 'サーバー環境の構築');
INSERT INTO tasks (title, status)
SELECT 'APIの設計レビュー', 'in_progress'
FROM dual WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE title = 'APIの設計レビュー');
INSERT INTO tasks (title, status)
SELECT 'テスト計画の策定', 'open'
FROM dual WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE title = 'テスト計画の策定');
INSERT INTO tasks (title, status)
SELECT '本番リリース準備', 'open'
FROM dual WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE title = '本番リリース準備');
SQL
echo "Initial data insertion completed."
mysql --default-character-set=utf8mb4 -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "SELECT id, title, status FROM tasks ORDER BY id;"
env:
- name: DB_HOST
value: "mysql-0.mysql-headless.db.svc.cluster.local"
- name: DB_NAME
value: "taskboard"
- name: DB_USER
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_PASSWORD
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
OUTER
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/db-init-job.yaml --dry-run=server
job.batch/db-init created (server dry run)
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/db-init-job.yaml
job.batch/db-init created
5.6.2 Job完了を確認しテーブル存在を検証する
Jobの完了を待機します。kubectl waitはJobの完了条件にも使用できます。
ここでは、先にTaskBoard APIをデプロイしてJPAにテーブルを作成させ、その後にJobの成功を確認する流れとします。Jobは内部でMySQLの起動を待機し、テーブルが存在するまで再試行するため、APIデプロイとJobの実行は並行して進められます。Jobの完了確認はStep 4の後に行います。
まず現時点でのJobの状態を確認しておきます。
[Execution User: developer]
kubectl get jobs -n db
NAME STATUS COMPLETIONS DURATION AGE
db-init Running 0/1 5s 5s
JobがRunning状態です。テーブルはまだ存在しないため(JPAがまだ動いていない)、Job内のSQLが失敗し、再試行を待っている可能性があります。Step 4でTaskBoard APIを起動した後に、改めてJobの完了を確認します。
5.7 Step 4 — アプリケーション層を構築する
データベース層が稼働しているので、アプリケーション層を構築します。TaskBoard API → Nginxの順にデプロイします。TaskBoard APIの起動時にJPAがMySQLにテーブルを自動生成するため、APIを先にデプロイする必要があります。
5.7.1 TaskBoard API Deploymentをデプロイする
TaskBoard API DeploymentとServiceを適用します。マニフェストは第3回で設計したもの(taskboard-api-deployment.yaml、taskboard-api-service.yaml)です。
TaskBoard APIのDeploymentとServiceのマニフェストを作成します。第3回の詳細設計で設計したSecurityContext、Probe、resources、RollingUpdate戦略がすべて含まれています。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/manifests/taskboard-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskboard-api
namespace: app
labels:
app: taskboard
component: api
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: taskboard
component: api
template:
metadata:
labels:
app: taskboard
component: api
spec:
containers:
- name: taskboard-api
image: taskboard-api:2.0.0
imagePullPolicy: Never
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "mysql-0.mysql-headless.db.svc.cluster.local"
- name: DB_NAME
value: "taskboard"
- name: DB_USER
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_PASSWORD
resources:
requests:
cpu: "200m"
memory: "384Mi"
limits:
cpu: "500m"
memory: "512Mi"
startupProbe:
httpGet:
path: /health/started
port: 8080
initialDelaySeconds: 0
periodSeconds: 2
timeoutSeconds: 3
failureThreshold: 30
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: payara-config
mountPath: /opt/payara/config
volumes:
- name: tmp
emptyDir: {}
- name: payara-config
emptyDir: {}
EOF
cat <<'EOF' > ~/k8s-production/manifests/taskboard-api-service.yaml
apiVersion: v1
kind: Service
metadata:
name: taskboard-api
namespace: app
labels:
app: taskboard
component: api
spec:
type: ClusterIP
selector:
app: taskboard
component: api
ports:
- port: 8080
targetPort: 8080
protocol: TCP
EOF
[Execution User: developer]
# dry-runで事前検証
kubectl apply -f ~/k8s-production/manifests/taskboard-api-deployment.yaml --dry-run=server
kubectl apply -f ~/k8s-production/manifests/taskboard-api-service.yaml --dry-run=server
deployment.apps/taskboard-api created (server dry run)
service/taskboard-api created (server dry run)
問題ないので適用します。
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/taskboard-api-deployment.yaml
kubectl apply -f ~/k8s-production/manifests/taskboard-api-service.yaml
deployment.apps/taskboard-api created
service/taskboard-api created
5.7.2 Payara Microの起動をkubectl waitで待機する
Payara Microの起動には15〜20秒かかります。応用編第8回で体験した通り、startupProbeが完了するまでlivenessProbeとreadinessProbeは実行されません。startupProbeの設計はperiodSeconds: 2, failureThreshold: 30(最大60秒待ち)です。
kubectl waitで全Podがready状態になるまで待機します。
[Execution User: developer]
# TaskBoard APIのPodがReady状態になるまで待機(最大90秒)
kubectl wait --for=condition=ready pod -l app=taskboard,component=api -n app --timeout=90s
pod/taskboard-api-6b8c9d7e8f-2x4k9 condition met
pod/taskboard-api-6b8c9d7e8f-m7h3j condition met
2つのPod(replicas: 2)がともにReady状態になりました。タイムアウトを90秒に設定しているのは、Payara Microの起動時間(15〜20秒)+ startupProbeの検知間隔 + α の余裕を持たせるためです。
Deploymentのロールアウト状態も確認します。
[Execution User: developer]
kubectl rollout status deployment/taskboard-api -n app
deployment "taskboard-api" successfully rolled out
kubectl rollout statusはDeploymentのロールアウトが完了したことを確認するコマンドです。kubectl waitが個々のPodの状態を確認するのに対し、rollout statusはDeployment全体の状態(指定したreplicas数のPodがAvailableか)を確認します。
5.7.3 ヘルスチェックエンドポイントで動作を検証する
PodがRunningでも、アプリケーションが正常に動作しているとは限りません。ヘルスチェックエンドポイントにアクセスして確認します。Payara Microのコンテナイメージにはcurlが含まれていないため、kubectl port-forwardでホストからアクセスします。
[Execution User: developer]
# port-forwardでホストからヘルスチェックエンドポイントを確認
kubectl port-forward -n app deploy/taskboard-api 18080:8080 > /dev/null 2>&1 &
sleep 2
curl -s http://localhost:18080/health/ready
kill %1 2>/dev/null
{"status":"UP","checks":[{"name":"db-connection","status":"UP","data":{"database":"taskboard"}}]}
"status":"UP"が返り、db-connectionチェックも"UP"です。TaskBoard APIがMySQLに正常接続できていることが確認できました。応用編第8回で追加したカスタムHealthCheckReady(DB接続確認を含む)が動作しています。
ここでDB初期化Jobの完了も確認しましょう。TaskBoard APIが起動してJPAがテーブルを作成したため、Jobの再試行が成功しているはずです。
[Execution User: developer]
kubectl get jobs -n db
NAME STATUS COMPLETIONS DURATION AGE
db-init Complete 1/1 35s 2m
JobのステータスがCompleteになりました。Jobのログを確認して、初期データが正常に投入されたことを検証します。
[Execution User: developer]
kubectl logs job/db-init -n db
Waiting for MySQL to be ready...
MySQL is ready.
Inserting initial data...
Initial data insertion completed.
+----+------------------------------------+-------------+
| ID | TITLE | STATUS |
+----+------------------------------------+-------------+
| 1 | プロジェクト計画の作成 | open |
| 2 | サーバー環境の構築 | open |
| 3 | APIの設計レビュー | in_progress |
| 4 | テスト計画の策定 | open |
| 5 | 本番リリース準備 | open |
+----+------------------------------------+-------------+
5件の初期データが投入されています。テーブルの存在と初期データの投入を確認できました。
5.7.4 Nginx Deploymentをデプロイする
次にNginx DeploymentとServiceを適用します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/manifests/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: app
labels:
app: taskboard
component: frontend
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: taskboard
component: frontend
template:
metadata:
labels:
app: taskboard
component: frontend
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 8080
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
livenessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
securityContext:
runAsNonRoot: true
runAsUser: 101
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
volumes:
- name: nginx-config
configMap:
name: nginx-config
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
EOF
cat <<'EOF' > ~/k8s-production/manifests/nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: app
labels:
app: taskboard
component: frontend
spec:
type: ClusterIP
selector:
app: taskboard
component: frontend
ports:
- port: 80
targetPort: 8080
protocol: TCP
EOF
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/nginx-deployment.yaml
kubectl apply -f ~/k8s-production/manifests/nginx-service.yaml
deployment.apps/nginx created
service/nginx created
Nginxは起動が非常に速い(1秒未満)ため、startupProbeは設定していません。kubectl waitで起動を待機します。
[Execution User: developer]
kubectl wait --for=condition=ready pod -l app=taskboard,component=frontend -n app --timeout=60s
pod/nginx-7d8e9f0a1b-4r6t8 condition met
pod/nginx-7d8e9f0a1b-k2m5n condition met
[Execution User: developer]
kubectl rollout status deployment/nginx -n app
deployment "nginx" successfully rolled out
Nginxの2つのPodがReady状態になりました。
5.7.5 SecurityContextの適用を確認する
第3回の詳細設計で、TaskBoard APIとNginxにSecurityContext(非root実行、readOnlyRootFilesystem、capabilities全剥奪、seccompProfile: RuntimeDefault)を設計しました。マニフェストを適用した後は、設計通りの設定がPodに反映されていることを確認します。
[Execution User: developer]
# TaskBoard APIのSecurityContextを確認
kubectl get pod -n app -l component=api -o jsonpath='{.items[0].spec.containers[0].securityContext}' | python3 -m json.tool
{
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": [
"ALL"
]
},
"readOnlyRootFilesystem": true,
"runAsNonRoot": true,
"runAsUser": 1000,
"seccompProfile": {
"type": "RuntimeDefault"
}
}
[Execution User: developer]
# NginxのSecurityContextを確認
kubectl get pod -n app -l component=frontend -o jsonpath='{.items[0].spec.containers[0].securityContext}' | python3 -m json.tool
{
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": [
"ALL"
]
},
"readOnlyRootFilesystem": true,
"runAsNonRoot": true,
"runAsUser": 101,
"seccompProfile": {
"type": "RuntimeDefault"
}
}
TaskBoard APIはrunAsUser: 1000(payaraユーザー)、NginxはrunAsUser: 101(nginxユーザー)で、ともにrunAsNonRoot: true、readOnlyRootFilesystem: true、capabilities.drop: ALL、seccompProfile: RuntimeDefaultが適用されています。応用編第7回で設計し、第3回の詳細設計書に記載したPSS Restricted準拠のSecurityContextが正しくデプロイされていることを確認できました。Step 4完了です。
5.8 Step 5-6 — 補助ワークロードとスケーリングを構築する
コアのアプリケーション層が稼働したので、補助ワークロード(CronJob、DaemonSet)とスケーリング・可用性(HPA、PDB)を追加します。
5.8.1 DBバックアップCronJobを適用する
DBバックアップCronJobのマニフェストは応用編第4回で作成したものです。ハンズオン用にschedule: "*/1 * * * *"(毎分実行)になっています。本番では"0 2 * * *"(毎日午前2時)のような日次スケジュールに変更してください。
[Execution User: developer]
cat <<'OUTER' > ~/k8s-production/manifests/db-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
namespace: db
labels:
app: taskboard
component: db-backup
spec:
schedule: "*/1 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
activeDeadlineSeconds: 120
template:
metadata:
labels:
app: taskboard
component: db-backup
spec:
restartPolicy: Never
containers:
- name: db-backup
image: mysql:8.0
command:
- sh
- -c
- |
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
echo "=== Backup started at ${TIMESTAMP} ==="
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" --single-transaction "$DB_NAME" > /tmp/backup-${TIMESTAMP}.sql
ls -lh /tmp/backup-${TIMESTAMP}.sql
echo "=== Backup preview (first 20 lines) ==="
head -20 /tmp/backup-${TIMESTAMP}.sql
echo "=== Backup completed successfully ==="
env:
- name: DB_HOST
value: "mysql-0.mysql-headless.db.svc.cluster.local"
- name: DB_NAME
value: "taskboard"
- name: DB_USER
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_PASSWORD
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
OUTER
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/db-backup-cronjob.yaml
cronjob.batch/db-backup created
CronJobリソースの作成を確認します。
[Execution User: developer]
kubectl get cronjobs -n db
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
db-backup */1 * * * * <none> False 0 <none> 5s
CronJobが作成されました。LAST SCHEDULEがまだ<none>なのは、最初のスケジュール実行がまだ発生していないためです。1分後に自動的にJobが作成されます。
5.8.2 ログ収集DaemonSetを適用する
ログ収集DaemonSetは応用編第4回で作成したものです。全Worker NodeにPodを配置します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/manifests/log-collector-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
namespace: monitoring
labels:
app: taskboard
component: log-collector
spec:
selector:
matchLabels:
app: taskboard
component: log-collector
template:
metadata:
labels:
app: taskboard
component: log-collector
spec:
containers:
- name: log-collector
image: busybox:1.36
command:
- sh
- -c
- |
echo "Log collector started on $(hostname)"
echo "Monitoring /var/log/containers/ ..."
while true; do
echo "--- $(date '+%Y-%m-%d %H:%M:%S') --- Log collection cycle ---"
ls /var/log/containers/*.log 2>/dev/null | wc -l | xargs -I{} echo " Container log files: {}"
sleep 60
done
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
EOF
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/log-collector-daemonset.yaml
daemonset.apps/log-collector created
DaemonSetのPodが全Worker Nodeに配置されたことを確認します。
[Execution User: developer]
kubectl get daemonset -n monitoring
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
log-collector 3 3 3 3 3 <none> 10s
DESIRED: 3、READY: 3です。Worker 3台それぞれにログ収集Podが配置されました。Podの配置先ノードも確認しておきます。
[Execution User: developer]
kubectl get pods -n monitoring -o wide
NAME READY STATUS RESTARTS AGE IP NODE
log-collector-4k8m2 1/1 Running 0 15s 10.244.1.12 k8s-applied-worker
log-collector-7j3n5 1/1 Running 0 15s 10.244.2.15 k8s-applied-worker2
log-collector-h2p9r 1/1 Running 0 15s 10.244.3.10 k8s-applied-worker3
3つのWorker Nodeそれぞれに1つずつPodが配置されています。DaemonSetの動作通りです。
5.8.3 HPA / PDBを適用する
HPA(HorizontalPodAutoscaler)とPDB(PodDisruptionBudget)を適用します。マニフェストは第3回で設計したものです。
[Execution User: developer]
# HPA(Nginx用)
cat <<'EOF' > ~/k8s-production/manifests/hpa-nginx.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
namespace: app
labels:
app: taskboard
component: frontend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 2
periodSeconds: 60
EOF
# HPA(TaskBoard API用)
cat <<'EOF' > ~/k8s-production/manifests/hpa-taskboard-api.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: taskboard-api-hpa
namespace: app
labels:
app: taskboard
component: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: taskboard-api
minReplicas: 2
maxReplicas: 4
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 1
periodSeconds: 60
EOF
# PDB(Nginx用)
cat <<'EOF' > ~/k8s-production/manifests/pdb-nginx.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: nginx-pdb
namespace: app
labels:
app: taskboard
component: frontend
spec:
minAvailable: 1
selector:
matchLabels:
app: taskboard
component: frontend
EOF
# PDB(TaskBoard API用)
cat <<'EOF' > ~/k8s-production/manifests/pdb-taskboard-api.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: taskboard-api-pdb
namespace: app
labels:
app: taskboard
component: api
spec:
minAvailable: 1
selector:
matchLabels:
app: taskboard
component: api
EOF
[Execution User: developer]
# HPAを適用
kubectl apply -f ~/k8s-production/manifests/hpa-nginx.yaml
kubectl apply -f ~/k8s-production/manifests/hpa-taskboard-api.yaml
# PDBを適用
kubectl apply -f ~/k8s-production/manifests/pdb-nginx.yaml
kubectl apply -f ~/k8s-production/manifests/pdb-taskboard-api.yaml
horizontalpodautoscaler.autoscaling/nginx-hpa created
horizontalpodautoscaler.autoscaling/taskboard-api-hpa created
poddisruptionbudget.policy/nginx-pdb created
poddisruptionbudget.policy/taskboard-api-pdb created
HPAの状態を確認します。Metrics Serverからメトリクスが取得できていることを確認します。
[Execution User: developer]
kubectl get hpa -n app
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx-hpa Deployment/nginx 4%/70% 2 6 2 15s
taskboard-api-hpa Deployment/taskboard-api 8%/70% 2 4 2 15s
TARGETS列にCPU使用率の現在値と閾値が表示されています。<unknown>/70%と表示される場合は、Metrics Serverがまだメトリクスを収集中です。1〜2分待ってから再確認してください。数値(例: 4%/70%)が表示されればメトリクスが正常に取得できています。
PDBの状態も確認します。
[Execution User: developer]
kubectl get pdb -n app
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
nginx-pdb 1 N/A 1 10s
taskboard-api-pdb 1 N/A 1 10s
MIN AVAILABLE: 1が設定され、ALLOWED DISRUPTIONS: 1です。現在replicas: 2で稼働しているため、1つのPodは退避可能(ALLOWED DISRUPTIONS: 1)であることを意味します。
5.8.4 全リソースの状態を一覧で確認する
Step 1〜6の適用が完了しました。全Namespaceのリソース状態を一覧で確認します。
[Execution User: developer]
# app Namespaceの全リソース
kubectl get all,pdb -n app
NAME READY STATUS RESTARTS AGE
pod/nginx-7d8e9f0a1b-4r6t8 1/1 Running 0 3m
pod/nginx-7d8e9f0a1b-k2m5n 1/1 Running 0 3m
pod/taskboard-api-6b8c9d7e8f-2x4k9 1/1 Running 0 4m
pod/taskboard-api-6b8c9d7e8f-m7h3j 1/1 Running 0 4m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx ClusterIP 10.96.50.100 <none> 80/TCP 3m
service/taskboard-api ClusterIP 10.96.50.200 <none> 8080/TCP 4m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 2/2 2 2 3m
deployment.apps/taskboard-api 2/2 2 2 4m
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler/nginx-hpa Deployment/nginx 4%/70% 2 6 2 1m
horizontalpodautoscaler/taskboard-api-hpa Deployment/taskboard-api 8%/70% 2 4 2 1m
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
poddisruptionbudget/nginx-pdb 1 N/A 1 1m
poddisruptionbudget/taskboard-api-pdb 1 N/A 1 1m
[Execution User: developer]
# db Namespaceの全リソース
kubectl get all,pvc -n db
NAME READY STATUS RESTARTS AGE
pod/db-init-xxxxx 0/1 Completed 0 5m
pod/mysql-0 1/1 Running 0 6m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/mysql-headless ClusterIP None <none> 3306/TCP 6m
NAME READY AGE
statefulset.apps/mysql 1/1 6m
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cronjob.batch/db-backup */1 * * * * False 0 30s 3m
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
persistentvolumeclaim/mysql-data-mysql-0 Bound pvc-... 1Gi RWO 6m
[Execution User: developer]
# monitoring Namespaceの全リソース
kubectl get all -n monitoring
NAME READY STATUS RESTARTS AGE
pod/log-collector-4k8m2 1/1 Running 0 2m
pod/log-collector-7j3n5 1/1 Running 0 2m
pod/log-collector-h2p9r 1/1 Running 0 2m
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE AGE
daemonset.apps/log-collector 3 3 3 3 3 2m
全Namespaceのリソースが設計書通りに配置されていることを確認できました。
5.9 Step 7 — 内部疎通テスト(受け入れテスト)
全コンポーネントが個別に稼働していることは確認できました。最後に、コンポーネント間の疎通を確認する受け入れテストを行います。VMの構築手順書で言えば「サーバー間通信テスト」に相当する段階です。
現時点ではNetworkPolicyが未適用のため、全Pod間で通信可能です。この状態で疎通を確認しておくことで、第6回でNetworkPolicyを適用した後に通信が遮断された場合、「NetworkPolicyが原因」と即座に切り分けられます。
5.9.1 テスト用Podから内部アクセスを確認する
テスト用のPodを起動し、Service経由で各コンポーネントにアクセスします。
[Execution User: developer]
# テスト用Podを起動(wgetが使えるbusyboxイメージ)
kubectl run test-client --image=busybox:1.36 -n app --rm -it --restart=Never -- sh
テスト用Podのシェルに入ったら、以下のコマンドを順に実行します。
# (1) Nginx Serviceへのアクセス確認
wget -qO /tmp/test.html http://nginx.app.svc.cluster.local/ && head -5 /tmp/test.html
# (2) TaskBoard API Serviceへのアクセス確認(タスク一覧の取得)
wget -qO- http://taskboard-api.app.svc.cluster.local:8080/taskboard-api/api/tasks
# (3) TaskBoard APIのヘルスチェック
wget -qO- http://taskboard-api.app.svc.cluster.local:8080/health/ready
テスト用Podのシェルからexitで抜けると、--rmオプションによりPodが自動削除されます。
テスト用Podを起動せずに、busyboxの一発実行でも確認できます。
[Execution User: developer]
# 方法2: busyboxの一発実行で確認(対話シェルを使わない方法)
kubectl run test-wget --image=busybox:1.36 -n app --rm -it --restart=Never \
-- wget -qO- http://nginx.app.svc.cluster.local/ 2>&1 | head -5
5.9.2 Nginx → API → MySQL の全経路を検証する
サービス間の全経路を検証します。NginxコンテナにもPayara Microコンテナにもwget/curlが含まれていないため、busyboxの一時Podから確認します。
[Execution User: developer]
# (1) Nginx Serviceの応答確認
kubectl run test-nginx --image=busybox:1.36 -n app --rm -it --restart=Never \
-- sh -c 'wget -qO /tmp/test.html http://nginx.app.svc.cluster.local/ && head -3 /tmp/test.html'
<!DOCTYPE html>
<html>
<head>
Nginxが静的ファイル(デフォルトのindex.html)を返しています。
[Execution User: developer]
# (2) TaskBoard API Serviceの応答確認(タスク一覧)
kubectl run test-api --image=busybox:1.36 -n app --rm -it --restart=Never \
-- wget -qO- http://taskboard-api.app.svc.cluster.local:8080/taskboard-api/api/tasks
[{"id":1,"title":"プロジェクト計画の作成","status":"open"},{"id":2,"title":"サーバー環境の構築","status":"open"},{"id":3,"title":"APIの設計レビュー","status":"in_progress"},{"id":4,"title":"テスト計画の策定","status":"open"},{"id":5,"title":"本番リリース準備","status":"open"}]
TaskBoard APIがMySQLからデータを取得し、JSON形式で返しています。DB初期化Jobで投入した5件のタスクがすべて取得できています。これにより、Nginx → TaskBoard API → MySQLの全経路が内部的に正常に機能していることが確認できました。
5.9.3 ヘルスチェックの正常性を確認する
最後に、各コンポーネントのヘルスチェック状態を確認します。
[Execution User: developer]
# TaskBoard APIのヘルスチェック(全エンドポイント)— port-forward経由で確認
kubectl port-forward -n app deploy/taskboard-api 18080:8080 > /dev/null 2>&1 &
sleep 2
curl -s http://localhost:18080/health
kill %1 2>/dev/null
{"status":"UP","checks":[{"name":"db-connection","status":"UP","data":{"database":"taskboard"}},{"name":"taskboard-api-live","status":"UP","data":{"version":"2.0.0"}}]}
/healthエンドポイントで全HealthCheckの結果を確認できます。db-connection(Readiness)とtaskboard-api-live(Liveness)がともに"UP"です。Livenessチェックにはバージョン情報("version":"2.0.0")も含まれており、デプロイされたイメージの確認にも使えます。
[Execution User: developer]
# Nginx のlivenessProbeと同じエンドポイントを確認(port-forward経由)
kubectl port-forward -n app deploy/nginx 18081:8080 > /dev/null 2>&1 &
sleep 2
curl -s http://localhost:18081/ | head -1
kill %1 2>/dev/null
<!DOCTYPE html>
NginxのlivenessProbe(HTTP GET / :8080)が正常に応答しています。
受け入れテストの結果をまとめます。
| テスト項目 | 対象 | 結果 |
|---|---|---|
| Nginx Service応答 | nginx.app.svc.cluster.local | ✔ HTML応答 |
| API Service応答 | taskboard-api.app.svc.cluster.local:8080/taskboard-api/api/tasks | ✔ JSON応答(5件) |
| API HealthCheck | localhost:8080/health/ready | ✔ UP(DB接続確認含む) |
| MySQL接続 | mysql-0.mysql-headless.db.svc.cluster.local:3306 | ✔ SELECT 1成功 |
| DB初期化 | Job db-init | ✔ Completed(5件投入) |
| CronJob作成 | CronJob db-backup | ✔ スケジュール登録済み |
| DaemonSet配置 | DaemonSet log-collector | ✔ Worker 3台に配置 |
| HPA メトリクス | nginx-hpa / taskboard-api-hpa | ✔ CPU使用率取得中 |
| PDB設定 | nginx-pdb / taskboard-api-pdb | ✔ minAvailable: 1 |
| SecurityContext | API(uid=1000)/ Nginx(uid=101) | ✔ PSS Restricted準拠 |
全項目が正常です。Gateway APIとNetworkPolicyを除く全コンポーネントが内部的に稼働し、疎通していることを確認しました。
5.10 この回のまとめ
5.10.1 成果物の確認 — 内部稼働中のTaskBoard + 構築手順書
本回で達成した状態を整理します。
[app Namespace]
Nginx Deployment (2 replicas) + Service
TaskBoard API Deployment (2 replicas) + Service
HPA (nginx-hpa: min 2 / max 6, taskboard-api-hpa: min 2 / max 4)
PDB (nginx-pdb: minAvailable 1, taskboard-api-pdb: minAvailable 1)
Secret (mysql-secret — API用)
ConfigMap (nginx-config — 非root対応版nginx.conf)
[db Namespace]
MySQL StatefulSet (1 replica) + Headless Service + PVC (1Gi)
Secret (mysql-secret — MySQL認証情報)
CronJob (db-backup — 定期バックアップ)
Job (db-init — 初期データ投入済み、Completed)
[monitoring Namespace]
DaemonSet (log-collector — Worker 3台に配置)
[未適用(第6回で適用)]
Gateway API (Gateway + HTTPRoute)
NetworkPolicy
本回で実行した手順そのものが、依存関係を考慮した構築手順書です。構築手順書として文書化する際には、以下の要素を含めます。
- 前提条件: 第4回で構築済みの基盤(Namespace、ResourceQuota、LimitRange、RBAC)
- 適用順序: Step 1(Secret/ConfigMap)→ Step 2(MySQL)→ Step 3(Job)→ Step 4(API/Nginx)→ Step 5(CronJob/DaemonSet)→ Step 6(HPA/PDB)
- 各ステップの検証項目: 本回で実施した検証コマンドと期待される出力
- 受け入れテスト: Step 7の全経路疎通確認
5.10.2 「適用順序」と「レイヤー検証」の重要性
本回を通じて、応用編との構築プロセスの違いが明確になったはずです。
| 観点 | 応用編の構築 | 実践編の構築(本回) |
|---|---|---|
| イメージビルド | 段階的(第1回→第3回→第8回) | 完成版を一括ビルド |
| 適用順序 | 学習テーマに従って適用 | 依存関係グラフに基づく計画的順序 |
| 検証 | 「動いた」で次に進む | 各レイヤーで検証してから次に進む |
| kubectl wait | 使わない(手動で確認) | 起動完了を待機してから次のステップへ |
| dry-run / diff | 使わない | 適用前に必ず事前確認する |
本番環境での構築では、「適用順序の設計」と「各レイヤーでの検証」が品質を左右します。依存関係を無視した順序で適用すると、エラーの原因が依存先の未準備なのか、マニフェスト自体の問題なのか切り分けが困難になります。レイヤーごとに検証することで、問題の発生箇所が明確になります。
5.10.3 次回予告 — 外部公開と通信制御
本回でTaskBoardは内部的に完全に稼働しています。しかし、外部からアクセスする手段がまだありません。また、現時点では全Pod間で通信可能な状態です。
次回(第6回)では以下を行います。
- Gateway APIの適用: パスベースルーティング(
/→ Nginx、/api→ TaskBoard API)で外部公開する - NetworkPolicyの適用: 最小権限の通信制御(API→DBのみ許可、フロント→DB直接は遮断)を適用する
- E2Eテスト: Client → Gateway → Service → Pod → DB の全経路を、外部からのアクセスも含めて検証する
本回の受け入れテストで確認した内部疎通が、NetworkPolicy適用後にどう変化するか。意図した通信のみが許可され、それ以外が遮断されることを検証します。本回の疎通テスト結果が、第6回のNetworkPolicy検証のベースラインになります。
AIコラム — 適用順序の相談
マニフェストの適用順序に迷ったとき、AIに相談すると依存関係の整理を手伝ってくれます。ただし、AIの出力をそのまま信頼するのではなく、自分の環境に合わせた検証が必要です。
💬 あなた → AI(Claude):
TaskBoardのマニフェスト一式をkindクラスタに適用したい。以下のリソースがある。適用順序を設計してほしい。
– MySQL StatefulSet + Headless Service
– TaskBoard API Deployment + Service
– Nginx Deployment + Service
– Secret(MySQL認証情報)
– ConfigMap(nginx.conf)
– DB初期化Job
– DBバックアップCronJob
– ログ収集DaemonSet
– HPA(Nginx用、API用)
– PDB(Nginx用、API用)
– Gateway API(Gateway + HTTPRoute)
– NetworkPolicy
AIは依存関係を分析し、以下のような順序を提案してくれるでしょう。
- Secret/ConfigMap → StatefulSet → Job → Deployment → CronJob/DaemonSet → HPA/PDB → Gateway API → NetworkPolicy
この提案は概ね正しいですが、確認すべきポイントがあります。
🔍 ここで立ち止まって確認しましょう
☑ Secretが2つのNamespaceに必要なことを考慮しているか? — db NamespaceとApp Namespaceの両方にSecretが必要です。AIがクロスNamespace制約を見落としている場合は補足が必要です
☑ DB初期化Jobの実行タイミングは適切か? — JPA(TaskBoard API)がテーブルを作成する前にJobを実行すると、テーブルが存在せずSQLが失敗します。Jobの再試行ロジック(backoffLimit)が適切に設計されているか確認が必要です
☑ NetworkPolicyを最後にする理由を理解しているか? — 「まず全通信可能な状態で疎通確認→その後NetworkPolicyで制限」の順序を取る理由は、トラブル切り分けの容易さです
☑ 各ステップの検証項目を含めているか? — AIは適用順序を提案しますが、「各ステップで何を検証するか」までは提案しないことが多いです。kubectl wait、ヘルスチェック確認、接続テストなどの検証項目は自分で設計する必要があります
AIは依存関係の整理と初期ドラフトの生成に有用ですが、環境固有の制約(Namespace間のSecret参照不可、Payara Microの起動時間、JPAのテーブル自動生成タイミング)は自分の知識で補完する必要があります。本回で実践した「Step 1〜7の適用順序 + 各ステップの検証」は、AIの提案をベースに環境固有の知識で磨き上げた結果です。
