本記事には広告(アフィリエイトリンク)が含まれます。

Kubernetes実践編 #03

【Kubernetes実践編 #03】詳細設計 — マニフェストの全パラメータを「意図を持って」決める

3.1 はじめに

3.1.1 前回の振り返り — 基本設計書が手元にある状態

前回(第2回)で、TaskBoardの基本設計書を作成しました。5つの設計領域について「何を使うか」と「なぜそれを選ぶか」を文書化しています。

  • ワークロード設計: Nginx → Deployment、TaskBoard API → Deployment、MySQL → StatefulSet、DB初期化 → Job、バックアップ → CronJob、ログ収集 → DaemonSet
  • 可用性設計: フロントとAPIは2レプリカ、MySQLは1レプリカ、PDBで最低1Podを維持
  • データ永続化設計: MySQL用PVC(1Gi、ReadWriteOnce)、CronJobによる定期バックアップ
  • ネットワーク設計: Service(ClusterIP)+ Gateway API(パスベースルーティング)
  • セキュリティ設計方針: Namespace分離(app / db / monitoring)、RBAC、NetworkPolicy(デフォルト拒否)、SecurityContext(非root必須)

基本設計書は「方針」を決める文書です。「MySQLにはStatefulSetを使用する(理由:データの永続化と安定したネットワークIDが必要)」——ここまでが基本設計の範囲でした。

3.1.2 本回の問題提起 — 「YAMLの各フィールドの値を根拠を持って決められるか」

基本設計書があっても、いざマニフェストを書こうとすると手が止まる瞬間があります。

「requests.memoryは256Miでいい?384Mi?根拠は?」
「startupProbeのfailureThresholdは何回にする?periodSecondsは?」
「readOnlyRootFilesystemをtrueにしたいけど、どのパスにemptyDirを設定すればいい?」

これらの問いに答えるのが「詳細設計」です。基本設計の「方針」を、YAMLマニフェストの「具体的なパラメータ値」に変換する作業です。そして、すべてのパラメータに「なぜこの値か」の根拠を添える。これが本回の仕事です。

3.1.3 本回のゴールと成果物

本回のゴールは、マニフェストの全フィールドについて「なぜこの値か」を説明できる状態になることです。

成果物は以下の2つです。

  • パラメータ設計書: 各マニフェストのフィールドごとに「値」と「設計根拠」を対にした文書。記事内で段階的に構築します
  • 全マニフェスト: 第4回〜第6回でkubectl applyする完成版マニフェスト一式。主要マニフェスト(Deployment、StatefulSet、Service、HPA、PDB)は全文掲載し、補助マニフェスト(Namespace、RBAC、NetworkPolicy等)は応用編からの変更点のみ記載します

本回は設計フェーズの最終回です。次回(第4回)からは構築フェーズに入り、ここで作成したマニフェストを実際にクラスタに適用します。本回の成果物の品質が、構築フェーズの成否を左右します。

3.2 VMのパラメータシートとK8sのパラメータ設計書

3.2.1 VMの世界でのパラメータシート — 何を記録するか

VMwareの世界で「詳細設計書」や「パラメータシート」を書いた経験がある方は、その構造を思い出してください。基本設計で「Webサーバー2台、DBサーバー1台」と決めた後、詳細設計ではVM1台1台のパラメータを詰めていきます。

  • VMスペック: vCPU数、メモリ容量、ディスク容量(各値に設計根拠を記載)
  • OS設定: タイムゾーン、NTP設定、ファイアウォールルール
  • ミドルウェア設定: Apacheのworker数、MySQLのinnodb_buffer_pool_size
  • 監視設定: Zabbixのポーリング間隔、リトライ回数、アラート閾値
  • バックアップ設定: スケジュール、保持世代数、取得方式

共通しているのは、すべてのパラメータに「なぜこの値か」の根拠が求められる点です。設計レビューで「このVMのメモリが4GBなのはなぜ?」と聞かれて「なんとなく」では通りません。「JVMヒープ2GB + OS領域1GB + バッファ1GB = 4GB」のように、積み上げの根拠が必要です。

3.2.2 K8sのパラメータ設計書 — 何を記録するか

K8sの詳細設計でも同じことをします。ただし、対象がVMの設定値からマニフェストのフィールド値に変わります。

VMのパラメータシートK8sのパラメータ設計書
vCPU数 / メモリ容量resources(requests / limits)
Zabbixポーリング間隔 / リトライ回数Probe(periodSeconds / failureThreshold)
サービスアカウント / SELinux設定SecurityContext(runAsUser / capabilities)
設定ファイルの外出し(Ansibleの変数)ConfigMap / Secret
デプロイ方式(ローリング / Blue-Green)strategy(RollingUpdate: maxSurge / maxUnavailable)
HA構成の同居制限(アンチアフィニティ)PDB(minAvailable / maxUnavailable)

技術は変わりましたが、「パラメータの値を決め、根拠を添える」というプロセスはまったく同じです。

3.2.3 「なぜこの値か」に答える設計文化

本回で作成するパラメータ設計書は、以下の構造を持ちます。各セクション(3.3〜3.7)の末尾で、その設計領域のパラメータ設計書パートを完成させていきます。

┌────────────────┬──────────────┬─────────────────────────────┐
│ パラメータ       │ 設定値        │ 設計根拠                       │
├────────────────┼──────────────┼─────────────────────────────┤
│ replicas       │ 2            │ 基本設計: 可用性確保(PDB考慮)   │
│ requests.cpu   │ 200m         │ Payara Micro定常時のCPU使用量   │
│ limits.memory  │ 512Mi        │ JVMヒープ256Mi+メタ+OS領域      │
│ startupProbe   │ fT:30, pS:2  │ 起動15-20秒、最大60秒待ち       │
└────────────────┴──────────────┴─────────────────────────────┘

この「値 + 根拠」のペアが、設計レビューでの「なぜこの値か」への回答になります。VMの設計レビューと同じ文化を、K8sの世界でも維持しましょう。

3.3 resources設計 — CPU/メモリの割り当て根拠

3.3.1 コンポーネント別の特性を整理する

resources(requests / limits)の値を決めるには、まず各コンポーネントの「性格」を理解する必要があります。応用編第1回でkubectl top podsを実行した際、NginxとPayara Microのリソース消費量に大きな差があったことを覚えているでしょうか。あの実測値が、resources設計の出発点になります。

コンポーネントCPU特性メモリ特性定常時の実測傾向
Nginx静的ファイル配信のみ。CPU消費は極めて小さい軽量。ワーカープロセスが少量のメモリを使用CPU: 1〜5m、メモリ: 20〜40Mi
TaskBoard API(Payara Micro)起動時に高負荷(クラスローディング)。定常時は中程度JVMヒープ + Metaspace + OS領域。起動後は比較的安定CPU: 10〜50m、メモリ: 280〜350Mi
MySQLクエリ処理に依存。TaskBoardの規模では低〜中程度innodb_buffer_pool_sizeが支配的。起動後は安定CPU: 10〜30m、メモリ: 150〜250Mi

requestsは「このPodを配置するためにNodeに確保してほしいリソース量」であり、Schedulerの配置判断に使われます。limitsは「このPodが使用できるリソースの上限」であり、これを超えるとOOMKill(メモリ)やスロットリング(CPU)が発生します。

3.3.2 TaskBoard API(Payara Micro)のresources設計

Payara MicroはJVMベースのアプリケーションサーバーです。メモリ設計にはJVM固有の考慮が必要です。

JVMのメモリは、大きく3つの領域で構成されます。ヒープ(オブジェクトの格納先)、Metaspace(クラス情報の格納先)、そしてネイティブメモリ(スレッドスタック、NIOバッファ等)です。Payara Micro 7.2026.1のデフォルト設定では、JVMヒープの最大サイズはコンテナのメモリ制限の約25%〜50%に自動調整されます(JVMのエルゴノミクス機能)。

limits.memoryを512Miに設定した場合、JVMは概ね以下のように領域を使います。

limits.memory: 512Mi の内訳(概算)
├── JVMヒープ       : 約200〜256Mi(エルゴノミクスによる自動調整)
├── Metaspace      : 約50〜80Mi(ロードするクラス数に依存)
├── スレッドスタック  : 約30〜50Mi(スレッド数 × 1MBスタック)
├── NIOバッファ等   : 約20〜30Mi
└── OS領域 + 余裕   : 残り(OOMKill防止のバッファ)

応用編第1回でkubectl topを実行した際、Payara Microは定常時に280〜350Mi程度のメモリを消費していました。limits.memoryを512Miに設定することで、定常時の消費量に対して約150〜230Miの余裕が確保されます。この余裕は、リクエスト増加時のヒープ拡張やGCの一時的なメモリ使用増に対応するバッファです。

requests.memoryは384Miとします。定常時の消費量(280〜350Mi)をカバーしつつ、Schedulerが適切にNodeへ配置できるだけの「予約量」です。requestsをlimitsと同じ512Miにすると、過剰な予約でNodeのリソースが無駄になります。逆に256Miまで下げると、実際の消費量がrequestsを常時上回り、Nodeのリソースが逼迫したときに真っ先に退避される対象になります。

CPUについては、requests: 200mが起動後の定常時に十分な値です。起動時にはクラスローディングで一時的にCPUが跳ね上がりますが、limitsの500mでキャップされるため、他のPodへの影響は限定的です。

3.3.3 Nginx のresources設計

Nginxは静的ファイル配信のみを担当する軽量コンポーネントです。CPU消費は極めて小さく、メモリも少量で済みます。

requests.cpuを50m、requests.memoryを64Miとします。これは応用編で観察した定常時のリソース消費(CPU: 1〜5m、メモリ: 20〜40Mi)に十分な余裕を持たせた値です。limits.cpuは200m、limits.memoryは128Miとし、トラフィック急増時にも対応できるようにします。

Nginxのresourcesは意図的に小さく設定しています。HPAでスケールアウトする際、1Pod追加あたりのリソース消費が小さいため、ResourceQuota(app: requests.cpu合計2コア)の中でより多くのPodを起動できます。応用編第8回で設定したHPA(min: 2、max: 6)でフルスケールアウトしても、Nginx 6 Pod × requests 50m = 300mで、Quotaには十分な余裕があります。

3.3.4 MySQL のresources設計

MySQLはデータベースという性質上、メモリの安定的な確保がパフォーマンスに直結します。特にinnodb_buffer_pool_sizeはMySQLのパフォーマンスを左右する最重要パラメータであり、利用可能なメモリの50〜80%を割り当てるのがベストプラクティスとされています。

limits.memoryを512Miに設定した場合、MySQLが実際に使えるメモリは512MiからOS領域やスレッド管理領域を差し引いた量になります。innodb_buffer_pool_sizeはデフォルトの128MBのままとし、TaskBoardの学習環境では十分なサイズです。本番環境でデータ量が増える場合は、limits.memoryの増加とともにinnodb_buffer_pool_sizeも調整します。

requests.memoryは256Miとします。MySQLは起動後にメモリ使用量が安定するため、requestsとlimitsの間に差を設けても問題ありません。ただし、limitsとの差が大きすぎると、Nodeのメモリが逼迫した際にOOMKillされるリスクが高まります。StatefulSetのPodは再起動時にデータの整合性チェックが走るため、不用意な再起動は避けたいところです。

3.3.5 resources設計のパラメータ設計書

コンポーネントパラメータ設定値設計根拠
Nginxrequests.cpu50m定常時CPU消費1〜5m。余裕を持たせつつ、HPA時のQuota効率を考慮
requests.memory64Mi定常時メモリ消費20〜40Mi。ワーカープロセスの安定動作に十分
limits.cpu200mトラフィック急増時の処理能力確保
limits.memory128Mi静的ファイル配信のみのため128Miで十分。OOMKill防止のバッファ含む
TaskBoard APIrequests.cpu200m定常時CPU消費10〜50m。JVMのGC処理やリクエスト処理の安定確保
requests.memory384Mi定常時メモリ消費280〜350Mi。Schedulerへの適切な予約量
limits.cpu500m起動時のクラスローディングおよびリクエストスパイク時に対応
limits.memory512MiJVMヒープ約256Mi + Metaspace約80Mi + スレッド/NIO約50Mi + OS/バッファ約126Mi
MySQLrequests.cpu200mクエリ処理の安定性確保。TaskBoardの規模では十分
requests.memory256Miinnodb_buffer_pool_size 128MB + OS領域 + バッファ
limits.cpu500m複雑なクエリやテーブルスキャン時のCPU確保
limits.memory512Miinnodb_buffer_pool_size 128MB + 接続スレッド + 一時テーブル + OS領域

3.4 Probe設計 — 「応用編の体験」を「設計判断」に変換する

3.4.1 3つのProbeの復習と設計判断の観点

応用編第8回で、3つのProbeの役割と各パラメータの効果を体験しました。ここでは、その体験を「TaskBoardの各コンポーネントにどのProbeをどの値で設定するか」という設計判断に変換します。

Probe判定結果失敗時のアクション設計判断の観点
startupProbe起動が完了したか完了前にliveness/readinessを抑制起動に時間がかかるコンポーネントに必須
livenessProbeプロセスが生きているかコンテナを再起動failureThresholdを小さくしすぎると不安定化
readinessProbeリクエストを受け入れ可能かServiceのエンドポイントから除外DB接続確認等の依存チェックを含められる

応用編第8回で体験した重要な教訓を振り返ります。startupProbeなしでPayara Microをデプロイしたとき、起動完了前にlivenessProbeが「応答しない → 異常 → 再起動」と判定し、CrashLoopBackOffに陥りました。また、failureThresholdを1にしたとき、一時的な高負荷でPodが不必要に再起動される事態も体験しました。これらの「体験」が、パラメータ値の「根拠」になります。

3.4.2 TaskBoard API(Payara Micro)のProbe設計

Payara MicroはMicroProfile Health標準のエンドポイントを持っています。本回では、応用編第8回で設定したカスタムHealthCheck(HealthCheckReady.java:DB接続確認を含むReadinessチェック)が追加済みの状態を前提とします。

startupProbe — Payara Microの起動時間は15〜20秒です。Node負荷が高い状況では25秒以上かかる可能性もあります。応用編第8回でfailureThreshold: 30, periodSeconds: 2(最大60秒待ち)を設定し、安定した起動を確認しました。この値をそのまま採用します。エンドポイントは/health/startedで、Payara Microの起動完了を正確に判定できます。

livenessProbe — プロセスの生存確認です。エンドポイントは/health/live。応用編第8回ではperiodSeconds: 10, failureThreshold: 3(30秒間の無応答で再起動)を設定しました。この設定は「一時的な高負荷で誤って再起動されない」と「本当にハングした場合に適切な時間で検知する」のバランスを取っています。failureThresholdを1にした場合の不安定さは応用編で体験済みです。

readinessProbe — リクエスト受け入れ可否の判定です。エンドポイントは/health/readyで、応用編第8回で追加したカスタムHealthCheckReady(DB接続確認を含む)が動作します。periodSeconds: 5でlivenessより高頻度に設定します。readinessProbeの失敗はServiceのエンドポイントからの除外のみで再起動は行われないため、頻繁にチェックしても副作用は小さいです。DB接続が切れた場合に素早くトラフィックから除外し、復旧したら素早くトラフィックに復帰させるためです。

3.4.3 Nginx のProbe設計

Nginxは起動が非常に速く(通常1秒以内)、MicroProfile Healthのようなヘルスチェック専用エンドポイントは持ちません。HTTP GETでルートパス(/)にアクセスし、応答を確認します。ポートは応用編第7回で非root化した際に80から8080に変更済みです。

startupProbeは設定しません。 Nginxの起動は1秒以内で完了するため、startupProbeを設ける必要はありません。livenessProbeのinitialDelaySecondsも不要(デフォルトの0秒)です。

livenessProbeHTTP GET /(port 8080)で応答を確認します。periodSeconds: 10, failureThreshold: 3で、30秒間の無応答で再起動します。Nginxがハングすることは稀ですが、万が一のワーカープロセス異常に備えた保険です。

readinessProbe — livenessと同じHTTP GET /(port 8080)を使用しますが、periodSeconds: 5, failureThreshold: 3で、15秒間応答しなければServiceから除外します。設定ファイルの誤りなどで応答不能になった場合に、トラフィックを正常なPodに寄せるための設定です。

3.4.4 MySQL のProbe設計

MySQLのProbeにはHTTPエンドポイントがないため、TCP接続チェック(port 3306)を使用します。MySQLにはmysqladmin pingというヘルスチェックコマンドもありますが、exec Probeはプロセス起動のオーバーヘッドがあります。TCP接続チェックは軽量かつ「MySQLプロセスがポートをlistenしているか」を十分に判定できます。

startupProbeは設定しません。 MySQLの起動は通常5〜10秒程度で完了します。Payara Microほど長くはないため、livenessProbeのinitialDelaySeconds: 10で対応できます。ただし、初回起動時(データディレクトリの初期化を含む場合)は20秒以上かかることがあるため、余裕を持たせます。

livenessProbetcpSocket port: 3306でMySQLプロセスの生存を確認します。initialDelaySeconds: 15, periodSeconds: 10, failureThreshold: 3です。データベースの再起動はデータ整合性に影響しうるため、failureThresholdは慎重に設定します。応用編第8回の教訓をここでも活かし、安易に再起動されない設定にします。

readinessProbe — 同じくtcpSocket port: 3306を使用します。periodSeconds: 5, failureThreshold: 3で、MySQLがクエリを受け付けられない状態を素早く検知し、Headless Serviceのエンドポイントから除外します。

3.4.5 Probe設計のパラメータ設計書

コンポーネントProbe方式パラメータ設計根拠
TaskBoard APIstartupProbeHTTP GET /health/started :8080initialDelaySeconds: 0, periodSeconds: 2, failureThreshold: 30, timeoutSeconds: 3起動15-20秒。最大60秒待ち。応用編第8回で検証済み
livenessProbeHTTP GET /health/live :8080periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 330秒間の無応答で再起動。一時的高負荷での誤再起動を防止
readinessProbeHTTP GET /health/ready :8080periodSeconds: 5, failureThreshold: 3, successThreshold: 1, timeoutSeconds: 3DB接続確認含む。15秒で除外、1回成功で復帰
NginxlivenessProbeHTTP GET / :8080periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 330秒間の無応答で再起動。起動が速いためstartupProbe不要
readinessProbeHTTP GET / :8080periodSeconds: 5, failureThreshold: 3, successThreshold: 1, timeoutSeconds: 315秒で除外。設定ミスによる応答不能に備える
MySQLlivenessProbetcpSocket :3306initialDelaySeconds: 15, periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 3初期化含む起動時間を考慮。DB再起動の影響が大きいため慎重に設定
readinessProbetcpSocket :3306periodSeconds: 5, failureThreshold: 3, successThreshold: 1, timeoutSeconds: 3クエリ受付不可を素早く検知。Headless Serviceエンドポイント制御

3.5 SecurityContext設計 — 各Podのセキュリティ設定値を決める

3.5.1 応用編第7回の知識を設計に落とし込む

応用編第7回では、SecurityContextの4つの設定項目を優先順位とともに学びました。「非rootで動かす → ファイルシステムを保護する → 権限昇格を防ぐ → ケーパビリティを最小化する」という順序です。この4項目をTaskBoardの各コンポーネントに適用する際の具体的な値を決定します。

応用編第7回で確認した各コンポーネントの実行ユーザーは以下の通りです。

コンポーネント実行ユーザーUID応用編第7回での対応
Nginxnginx101ポート80→8080変更、nginx.confをConfigMap化、emptyDirマウント
TaskBoard API(Payara Micro)payara1000元から非root。SecurityContext宣言のみ追加
MySQLmysql999元から非root。readOnlyRootFilesystemは非適用

3.5.2 コンポーネント別のSecurityContext設計

全コンポーネント共通の設定:

  • runAsNonRoot: true — rootでの実行を禁止
  • allowPrivilegeEscalation: false — 権限昇格を禁止
  • capabilities.drop: ALL — 全ケーパビリティを剥奪

Nginx: runAsUser: 101(nginx:1.27イメージのnginxユーザー)。readOnlyRootFilesystem: trueを適用し、/tmp(PID、一時ファイル、ログ)と/var/cache/nginx(キャッシュ)をemptyDirでマウントします。nginx.confはConfigMapで/etc/nginx/nginx.confにsubPathマウントします。応用編第7回で非root対応済みのnginx.conf(listen 8080、PID/一時ファイル/ログを/tmp配下に変更)を使用します。

TaskBoard API(Payara Micro): runAsUser: 1000(payaraユーザー)。readOnlyRootFilesystem: trueを適用し、/tmp(Java一時ファイル領域)と/opt/payara/config(Payara実行時設定)をemptyDirでマウントします。Payara Microは元から非rootで動作するため、SecurityContextの追加で動作に影響はありません。

MySQL: runAsUser: 999(mysqlユーザー)。readOnlyRootFilesystemは適用しません。応用編第7回で判断した通り、MySQLは起動時に/var/lib/mysql以外にも複数のディレクトリ(/var/lib/mysql-files/var/lib/mysql-keyring等)への書き込みを必要とし、これらすべてをemptyDirで対応する費用対効果が低いためです。代わりに/tmp/var/run/mysqld(ソケットファイル)をemptyDirでマウントします。データディレクトリ/var/lib/mysqlはvolumeClaimTemplatesによるPVCマウントです。

3.5.3 SecurityContext設計のパラメータ設計書

コンポーネントパラメータ設定値設計根拠
NginxrunAsNonRoottrue非root実行を強制
runAsUser101nginx:1.27のnginxユーザー(uid=101)
readOnlyRootFilesystemtrue書き込みはemptyDir(/tmp, /var/cache/nginx)に限定
allowPrivilegeEscalationfalse権限昇格を禁止
capabilities.dropALL全ケーパビリティ剥奪
emptyDirマウント/tmp, /var/cache/nginxNginx動作に必要な書き込みパス
TaskBoard APIrunAsNonRoottrue非root実行を強制
runAsUser1000Payara Microのpayaraユーザー(uid=1000)
readOnlyRootFilesystemtrue書き込みはemptyDir(/tmp, /opt/payara/config)に限定
allowPrivilegeEscalationfalse権限昇格を禁止
capabilities.dropALL全ケーパビリティ剥奪
emptyDirマウント/tmp, /opt/payara/configJVM一時ファイルとPayara設定ファイル用
MySQLrunAsNonRoottrue非root実行を強制
runAsUser999mysql:8.0のmysqlユーザー(uid=999)
readOnlyRootFilesystemfalse(非適用)MySQL初期化プロセスが複数パスに書き込むため費用対効果が低い
allowPrivilegeEscalationfalse権限昇格を禁止
capabilities.dropALL全ケーパビリティ剥奪
emptyDirマウント/tmp, /var/run/mysqld一時ファイルとソケットファイル用。データは PVC

3.6 ConfigMap / Secret設計 — 何を外出しにするか

3.6.1 ConfigMapに格納するもの

ConfigMapには、マニフェストから分離すべき「設定情報」を格納します。TaskBoardで必要なConfigMapは以下の1つです。

nginx-config: 応用編第7回で作成した非root対応版のnginx.confを格納します。listen 8080、PID/一時ファイル/ログを/tmp配下に配置する設定です。Nginx DeploymentからsubPathマウントで/etc/nginx/nginx.confに配置します。subPathを使う理由は、/etc/nginxディレクトリ内の他のファイル(mime.types等)を上書きしないためです。

TaskBoard API(Payara Micro)の設定は、現状ではDockerイメージのビルド時に組み込まれています。DB接続情報はSecretとenv経由で渡します。ConfigMapとして外出しにする設定は現時点ではありません。実践編第8回でHelm化する際に、values.yamlに切り出す項目として改めて検討します。

3.6.2 Secretに格納するもの

Secretには、平文で保管すべきでない「機密情報」を格納します。

mysql-secret(db Namespace): MySQL認証情報を格納します。応用編第3回で作成したSecretと同じ構造です。

  • MYSQL_ROOT_PASSWORD — MySQLのrootパスワード
  • MYSQL_DATABASE — 作成するデータベース名(taskboard)
  • MYSQL_USER — アプリケーション用ユーザー名
  • MYSQL_PASSWORD — アプリケーション用パスワード

MySQL StatefulSetからはenvFrom.secretRefで全キーを環境変数として注入します。TaskBoard API Deploymentからはenv.valueFrom.secretKeyRefMYSQL_USERMYSQL_PASSWORDを個別に参照します。Secretはdb Namespaceに配置しますが、TaskBoard APIはapp Namespaceにあるため、クロスNamespace参照はできません。TaskBoard APIではDB接続情報を環境変数(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD)として設定し、DB_USERDB_PASSWORDはdb NamespaceのSecretではなく、app Namespaceにもmysql-secretを作成して参照します。

3.6.3 変更時の反映方法

ConfigMapやSecretを変更した場合、Podへの反映方法はマウント方式によって異なります。

マウント方式自動反映反映タイミングTaskBoardでの該当
volumeMount(subPathなし)ありkubeletの同期間隔(デフォルト約60秒)
volumeMount + subPathなしPod再起動が必要nginx-config(subPath使用)
env / envFromなしPod再起動が必要mysql-secret(envFrom使用)、API環境変数

TaskBoardでは、nginx-configのsubPathマウントとSecretのenv参照、いずれも変更時にはPodの再起動が必要です。実践編第8回で変更管理の手順を設計する際に、kubectl rollout restartを使った安全な再起動手順を整備します。

3.7 Deployment / StatefulSetの詳細パラメータ

3.7.1 strategy(ローリングアップデート戦略)

Deployment(Nginx、TaskBoard API)のアップデート戦略を決めます。

Nginx: RollingUpdatemaxSurge: 1, maxUnavailable: 0を設定します。maxUnavailableを0にすることで、アップデート中に利用不可のPodが生じないことを保証します。maxSurge: 1により、古いPodが停止する前に新しいPodが1つ起動し、容量を維持します。Nginxは起動が速いため、この設定でアップデートはスムーズに完了します。

TaskBoard API: 同じくRollingUpdatemaxSurge: 1, maxUnavailable: 0を設定します。Payara Microは起動に15〜20秒かかりますが、startupProbeが起動完了を監視するため、新しいPodが「Ready」になるまで古いPodは停止されません。maxUnavailable: 0により、アップデート中もAPIの可用性を維持します。

MySQL(StatefulSet): StatefulSetのデフォルト戦略はRollingUpdateです。replicasが1のため、strategyの設定による差はありません。アップデート時はmysql-0が停止し、新しいmysql-0が起動します。この間DBは一時的に停止しますが、TaskBoard APIのreadinessProbe(DB接続確認)がDBの復旧を検知し、トラフィックを自動で復元します。

3.7.2 ラベル体系の設計

マニフェスト間の整合性を確保するため、ラベル体系を統一します。応用編を通じて使用してきたラベル体系をそのまま踏襲します。

ラベルキー用途値の例
appアプリケーション全体の識別taskboard(全リソース共通)
componentコンポーネントの識別frontend / api / db / log-collector

この2つのラベルを組み合わせることで、Serviceのselector、NetworkPolicyのpodSelector、HPAのscaleTargetRefが正確にPodを選択できます。全マニフェストでこのラベル体系を一貫して使用することが、マニフェスト間の整合性の基盤です。

ラベルの配置場所も統一します。

metadata:
  labels:            # (1) リソース自体のラベル(検索・フィルタ用)
    app: taskboard
    component: api
spec:
  selector:
    matchLabels:     # (2) Deployment/StatefulSetがPodを選択するラベル
      app: taskboard
      component: api
  template:
    metadata:
      labels:        # (3) Pod templateのラベル — (2)と一致必須
        app: taskboard
        component: api

(2)と(3)が一致しないとDeploymentが自分のPodを管理できません。全マニフェストでこの一致を確認します。

3.7.3 PodDisruptionBudgetの値を決める

PDB(PodDisruptionBudget)は、Node drainやクラスタメンテナンス時に「最低何Podを維持するか」を保証するリソースです。基本設計で「フロントとAPIにPDBを設定する」と方針を決めました。ここで具体的な値を決めます。

Nginx: replicas: 2のうち、minAvailable: 1を設定します。これにより、Node drain時に2Podのうち少なくとも1Podは常に稼働していることが保証されます。2Podとも同一Nodeに配置されていた場合でも、1Podが別Nodeに移動してReadyになるまで、もう1Podの停止は待機されます。

TaskBoard API: 同じくminAvailable: 1を設定します。Payara Microは起動に15〜20秒かかるため、PDBの保護がなければdrain時にAPIが一時的に全停止する可能性があります。startupProbeとPDBの組み合わせにより、新しいPodが起動完了するまで古いPodが保護されます。

MySQL: replicas: 1のためPDBは設定しません。replicas: 1でminAvailable: 1のPDBを設定すると、Node drainが永遠にブロックされる可能性があるためです。MySQLのNode退避はサービス断を伴いますが、これはシングルレプリカ構成の制約として基本設計で受容済みです。

3.8 完成マニフェストの全体確認

3.8.1 主要マニフェストの掲載

ここまでの設計を反映した主要マニフェストを掲載します。パラメータ設計書と対照しながら確認してください。補助マニフェスト(Namespace、ResourceQuota、LimitRange、RBAC、NetworkPolicy、Gateway API関連)は応用編で作成済みのものを使用し、変更はありません。第4回〜第6回でそれらも含めて適用します。

■ TaskBoard API Deployment

# 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                      # 新Pod 1つを先に起動
      maxUnavailable: 0                # 利用不可Podをゼロに維持
  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       # kindクラスタ用
          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"              # JVM定常時のCPU
              memory: "384Mi"          # 定常時メモリ280-350Mi + 余裕
            limits:
              cpu: "500m"              # 起動時/スパイク時のCPU
              memory: "512Mi"          # JVMヒープ+Meta+スレッド+OS
          # Probe設計
          startupProbe:
            httpGet:
              path: /health/started
              port: 8080
            initialDelaySeconds: 0     # 即座にチェック開始
            periodSeconds: 2           # 2秒ごとにチェック
            timeoutSeconds: 3
            failureThreshold: 30       # 最大60秒待ち(2秒×30回)
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            periodSeconds: 10          # 10秒ごとにチェック
            timeoutSeconds: 3
            failureThreshold: 3        # 30秒間の無応答で再起動
          readinessProbe:
            httpGet:
              path: /health/ready      # DB接続確認含む
              port: 8080
            periodSeconds: 5           # livenessより高頻度
            timeoutSeconds: 3
            failureThreshold: 3        # 15秒で除外
            successThreshold: 1        # 1回成功で復帰
          # SecurityContext
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000            # payaraユーザー
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: payara-config
              mountPath: /opt/payara/config
      volumes:
        - name: tmp
          emptyDir: {}
        - name: payara-config
          emptyDir: {}

■ Nginx Deployment

# 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      # 非root対応で80→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             # nginxユーザー
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            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: {}

■ MySQL StatefulSet

# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: db
  labels:
    app: taskboard
    component: db
spec:
  serviceName: mysql-headless          # Headless Serviceと紐付け
  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             # mysqlユーザー
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            # readOnlyRootFilesystem: 非適用(設計根拠は3.5.2参照)
          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               # 学習環境のため1GiBで十分

■ Service定義

# taskboard-api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  selector:
    app: taskboard
    component: api
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
---
# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: app
  labels:
    app: taskboard
    component: frontend
spec:
  selector:
    app: taskboard
    component: frontend
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
---
# mysql-headless.yaml(Headless Service)
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  namespace: db
  labels:
    app: taskboard
    component: db
spec:
  clusterIP: None                      # Headless Service
  selector:
    app: taskboard
    component: db
  ports:
    - port: 3306
      targetPort: 3306
      name: mysql

■ HPA

# hpa-nginx.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-hpa
  namespace: app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx
  minReplicas: 2                       # Deployment.replicasと一致
  maxReplicas: 6
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70       # CPU requests(50m)の70%=35m超過でスケール
---
# hpa-taskboard-api.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: taskboard-api-hpa
  namespace: app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: taskboard-api
  minReplicas: 2
  maxReplicas: 4                       # JVMのリソース消費が大きいため控えめ
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      policies:
        - type: Pods
          value: 1                     # 1 Podずつ慎重にスケールアウト
          periodSeconds: 60

■ PDB

# pdb-nginx.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-pdb
  namespace: app
spec:
  minAvailable: 1                      # 常に最低1 Podを維持
  selector:
    matchLabels:
      app: taskboard
      component: frontend
---
# pdb-taskboard-api.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: taskboard-api-pdb
  namespace: app
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: taskboard
      component: api

3.8.2 マニフェスト間の整合性チェック

全マニフェストを書き上げたら、以下の観点でクロスチェックを行います。構築フェーズで「applyしたら動かない」を防ぐための最終確認です。

チェック観点確認内容TaskBoardでの確認結果
ラベル一致Deployment/StatefulSetのselector ↔ Pod templateのlabels全リソースで app: taskboard + component: xxx が一致 ✓
Service selectorServiceのselector ↔ 対象PodのラベルServiceのport ↔ Podのporttaskboard-api Service → component: api、port: 8080 ✓
nginx Service → component: frontend、port: 8080 ✓
mysql-headless → component: db、port: 3306 ✓
Secret参照envFrom/env.secretKeyRef → Secret名とキーの存在MySQL → mysql-secret(envFrom)✓
TaskBoard API → mysql-secret(DB_USER, DB_PASSWORD)✓
ConfigMap参照volumeMount → ConfigMap名とキーの存在Nginx → nginx-config ConfigMap、キー: nginx.conf ✓
Namespace全リソースが正しいNamespaceに配置されているかapp: Nginx, TaskBoard API, Service, HPA, PDB ✓
db: MySQL, mysql-headless, mysql-secret ✓
ポート番号containerPort ↔ Service port ↔ ProbeのportTaskBoard API: 全て8080 ✓
Nginx: 全て8080 ✓
MySQL: 全て3306 ✓
HPA対象scaleTargetRef ↔ Deployment名nginx-hpa → Deployment/nginx ✓
taskboard-api-hpa → Deployment/taskboard-api ✓
PDB対象selector ↔ Podラベルnginx-pdb → frontend ✓
taskboard-api-pdb → api ✓

この整合性チェック表は、構築フェーズ(第4回〜第6回)でも活用します。

3.8.3 パラメータ設計書の最終版

3.3〜3.7で段階的に構築してきたパラメータ設計書を、最終版として統合します。以下は主要パラメータの一覧です。

💬 あなた → AI(Claude):
TaskBoard APIのDeploymentマニフェストを作ってほしい。要件は以下の通り。
– replicas: 2
– イメージ: taskboard-api:2.0.0
– DB接続情報はSecretから取得
– MicroProfile Healthのエンドポイントでヘルスチェック
– 非root実行必須

🤖 Claude の回答:

# AIが生成したドラフト
apiVersion: apps/v1
kind: Deployment
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: taskboard
      component: api
  template:
    metadata:
      labels:
        app: taskboard
        component: api
    spec:
      containers:
        - name: taskboard-api
          image: taskboard-api:2.0.0
          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
            failureThreshold: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000

🔍 ここで立ち止まって確認しましょう
AIの出力をそのまま適用する前に、応用編で得た知識を基に以下をチェックしてください。

startupProbeの待ち時間は十分か?failureThreshold: 10 × periodSeconds: 5 = 最大50秒。一見十分に見えますが、応用編第8回でfailureThreshold: 30, periodSeconds: 2 = 最大60秒を採用した理由を思い出してください。periodSecondsが5秒だと、起動完了の検知が最大5秒遅れます。periodSeconds: 2なら最大2秒で検知できます。起動時間に変動がある環境では、チェック頻度を上げるほうがスムーズです
readinessProbeのperiodSecondsはlivenessと同じでいいか? — AIは両方をperiodSeconds: 10にしています。しかし、readinessProbeはServiceのルーティング制御に使われるため、livenessより高頻度(periodSeconds: 5)にするのが実践的です。DB接続が復旧した際に、素早くトラフィックに復帰させたいためです
SecurityContextは完全か?runAsNonRootrunAsUserは設定されていますが、readOnlyRootFilesystemallowPrivilegeEscalationcapabilitiesが欠落しています。応用編第7回で学んだ4つの設定項目をすべて含めるべきです
emptyDirのマウントは設定されているか?readOnlyRootFilesystem: trueにする場合、/tmp/opt/payara/configのemptyDirマウントが必要です。AIのドラフトにはvolumeMounts/volumesセクションがありません
strategyは指定されているか? — ローリングアップデート戦略が未指定です。デフォルトでRollingUpdateになりますが、maxSurge/maxUnavailableを明示的に設定しておくと設計意図が明確になります
imagePullPolicyは設定されているか? — kindクラスタではimagePullPolicy: Neverが必要です

AIの出力は「おおよそ正しい」ですが、応用編で培った知識があれば改善点を見つけられます。特にstartupProbeのパラメータとSecurityContextの完全性は、応用編第7回・第8回を体験していないと気づけない項目です。AIはドラフト生成に有用ですが、最終的な設計判断はあなた自身の経験と知識で行ってください。

補助マニフェスト(Namespace、ResourceQuota、LimitRange、RBAC、NetworkPolicy、Gateway API、CronJob、Job、DaemonSet)は応用編で作成したものをそのまま使用します。変更が必要な項目はありません。第4回〜第6回で適用する際にこれらのマニフェストも含めた全量を掲載します。

3.9 この回のまとめ

3.9.1 成果物の確認 — 全マニフェスト + パラメータ設計書

本回で以下の成果物が完成しました。

  • パラメータ設計書: resources(3.3節)、Probe(3.4節)、SecurityContext(3.5節)、ConfigMap/Secret(3.6節)、strategy/ラベル/PDB(3.7節)の各設計領域について、全パラメータの値と設計根拠を文書化
  • 主要マニフェスト: TaskBoard API Deployment、Nginx Deployment、MySQL StatefulSet、Service 3種、HPA 2種、PDB 2種
  • マニフェスト間整合性チェック表: ラベル、ポート、Secret参照、Namespace、HPA/PDB対象の整合性を確認

全マニフェストのすべてのフィールドに「なぜこの値か」の根拠があります。これが詳細設計の目的でした。

3.9.2 設計フェーズの振り返り(第1回〜第3回)

設計フェーズ全体を振り返ります。3回を通じて、TaskBoardの設計は以下のように具体化されてきました。

成果物抽象度答える問い
第1回(構成図)3階層の構成図高い「何があるか」「どうつながっているか」
第2回(基本設計)5領域の基本設計書中程度「何を使うか」「なぜそれを選ぶか」
第3回(詳細設計)全マニフェスト + パラメータ設計書低い(具体的)「具体的にどの値を設定するか」「なぜその値か」

構成図 → 方針 → パラメータ値。この3段階の具体化プロセスは、VMの世界でも、K8sの世界でも、あるいはこれから出会う新しい技術の世界でも、変わりません。技術は変わっても、設計のプロセスは持ち運べるスキルです。

3.9.3 次回予告 — 設計フェーズから構築フェーズへ

設計フェーズはこれで完了です。手元には構成図、基本設計書、詳細設計書(全マニフェスト + パラメータ設計書)が揃っています。

次回(第4回)からは構築フェーズに入ります。応用編で作成したTaskBoardリソースを一度すべて削除し、設計書に基づいて「白紙から」再構築します。まずは基盤整備——Namespace、ResourceQuota、LimitRange、RBAC、Metrics Server、Gateway APIコントローラーの導入です。今回作成したマニフェストを実際にkubectl applyするのは第5回からですが、基盤が正しく整備されていなければマニフェストは機能しません。

設計を「絵に描いた餅」で終わらせず、動くシステムとして実現する。次回からはその工程です。

AI活用 — マニフェストのドラフト生成と批判的レビュー:対話ログ

本回のAI活用セッションは、3.8.3節に組み込みました。AIにTaskBoard APIのDeploymentマニフェストのドラフト生成を依頼し、応用編で得た知識を基に批判的レビューを行う体験です。AIの出力は「おおよそ正しいが完全ではない」ことが多く、以下の改善点を発見できました。

  • startupProbeのperiodSecondsが大きすぎる(5秒 → 2秒に修正)— 応用編第8回の知見
  • readinessProbeのperiodSecondsがlivenessと同じ — トラフィック制御にはより高頻度が望ましい
  • SecurityContextが不完全 — readOnlyRootFilesystemallowPrivilegeEscalationcapabilitiesが欠落 — 応用編第7回の知見
  • emptyDirマウントが未設定 — readOnlyRootFilesystem: trueとの組み合わせに必須
  • imagePullPolicy: Neverが未設定 — kind環境固有の設定

AIは設計のたたき台を素早く作るのに適しています。しかし、パラメータの「最適値」と「設計根拠」は、自分の経験と知識で判断する必要があります。応用編で手を動かして得た体験こそが、AIの出力を検証するための最強の武器です。