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

Kubernetes実践編 #07

【Kubernetes実践編 #07】運用設計 — 「動き続ける」ための仕組みを作る

7.1 はじめに

7.1.1 構築フェーズの振り返り — TaskBoardは完全稼働している

第4回から第6回までの構築フェーズが完了しました。TaskBoardの現在の状態を確認しましょう。

[クラスタ全体]
  kindクラスタ(CP 1 + Worker 3)全ノードReady
  Calico CNI / Metrics Server / Gateway APIコントローラー 稼働中

[app namespace]
  Nginx Deployment (replicas: 2) + Service (ClusterIP) + HPA + PDB
  TaskBoard API Deployment (replicas: 2) + Service (ClusterIP) + HPA + PDB
  NetworkPolicy(デフォルト拒否 + Gateway許可)

[db namespace]
  MySQL StatefulSet (replicas: 1) + Headless Service + PVC
  DB初期化Job (Completed) / DBバックアップCronJob
  Secret / NetworkPolicy(デフォルト拒否 + API許可)

[monitoring namespace]
  ログ収集DaemonSet(全Worker Node)

[外部アクセス]
  Gateway API(/ → Nginx, /api → TaskBoard API)

[E2Eテスト]
  全テスト項目パス済み

設計書通りに構築し、E2Eテストをパスし、全コンポーネントが正常稼働しています。構築フェーズの成果物——動くTaskBoard——は手元にあります。

7.1.2 本回の問題提起 — 「動いた。で、運用はどうする?」

VMの世界を思い出してください。サーバーを構築して「動いた!」で終わるプロジェクトはありません。構築報告書の次に待っているのは、運用設計書です。監視はどうするか。バックアップはいつ取るか。パッチ適用はどう進めるか。障害が起きたら誰が何をするか。

TaskBoardも同じです。HPA、PDB、CronJobは第5回で適用済みですが、それは「設計書に書いてあったからapplyした」段階です。「HPAの閾値70%は本当に適切か」「バックアップの保持世代数は何世代か」「ローリングアップデート中にサービス断は起きないか」——これらの問いに答えられる運用設計書は、まだ存在しません。

構築フェーズでは「設計書通りに動くこと」が目標でした。運用フェーズでは「動き続けること」が目標になります。そしてそのためには、稼働中のシステムを観察し、メトリクスに基づいた運用設計書を作成する必要があります。

7.1.3 本回のゴールと成果物

本回のゴールは、TaskBoardの運用設計書を作成することです。稼働中のTaskBoardのメトリクスを観察し、負荷テストで挙動を検証し、バックアップからのリストアをテストしながら、4つの設計領域をカバーする運用設計書を段階的に完成させます。

TaskBoard 運用設計書
├── 1. 監視設計
│     ├── 1.1 監視項目一覧(コンポーネント、監視対象、閾値、アクション)
│     ├── 1.2 Probeによる自動復旧設計
│     └── 1.3 リソース監視(CPU/メモリの定常値と閾値)
├── 2. スケーリング設計
│     ├── 2.1 HPA設定(閾値、min/max、クールダウン)
│     ├── 2.2 スケーリングの上限(ResourceQuotaとの関係)
│     └── 2.3 負荷テスト結果
├── 3. デプロイ戦略
│     ├── 3.1 ローリングアップデート設定(maxSurge、maxUnavailable)
│     ├── 3.2 デプロイ手順(dry-run → diff → apply → 確認)
│     └── 3.3 ロールバック手順
└── 4. バックアップ設計
      ├── 4.1 バックアップスケジュールと保持ポリシー
      ├── 4.2 バックアップ手順
      ├── 4.3 リストア手順
      └── 4.4 リストアテスト結果

本回は「運用設計書を書く」回ですが、設計の根拠を得るためのハンズオンを伴います。kubectl topで定常値を観察し、stress-ngで負荷テストを行い、バックアップからのリストアを実際に試す。「実機でメトリクスを確認 → その結果を設計書に反映」というフローを繰り返します。設計フェーズ(第1〜3回)の座学とも構築フェーズ(第4〜6回)のハンズオンとも異なる、運用フェーズ独自のリズムです。

7.2 VMの運用設計とK8sの運用設計

7.2.1 VMの世界での運用設計書 — 何を書くか

VMの世界で「運用設計書を書いてください」と言われたら、次のような文書を作成するのが一般的です。

文書名記載内容主な読み手
監視設計書Zabbix/NagiosのアイテムID、ポーリング間隔、閾値、エスカレーションフロー運用チーム
バックアップ設計書VSS/スナップショットのスケジュール、保持世代数、テープ退避有無、リストア手順運用チーム / DR担当
デプロイ手順書パッチ適用手順、ブルーグリーン/カナリアの切替手順、切戻し判断基準リリースチーム
キャパシティ計画書CPU/メモリの増設基準、VM追加のリードタイム、予算申請時期管理職 / インフラ責任者

これらの文書に共通しているのは、「何を」「どの値で」「なぜその値で」「異常時にどうするか」という4つの要素です。監視設計書であればCPU使用率80%をアラート閾値にする理由、バックアップ設計書であれば日次バックアップの保持を7世代にする理由。値だけでなく、その値を選んだ根拠がセットで記載されます。

7.2.2 K8sの運用設計書 — 何を書くか

K8sの運用設計書でも、「何を」「どの値で」「なぜその値で」「異常時にどうするか」という構造は同じです。ただし、K8sにはVMにはない自動化の仕組みが標準で組み込まれているため、運用設計書の内容が変わります。

運用設計領域VMの世界Kubernetesの世界
監視Zabbix/Nagiosで外部から監視。エージェントをVMにインストールProbeがコンテナ内部からヘルスチェック。異常時は自動復旧(再起動 or Service除外)
スケーリングアラート → 手動でVM追加 → LBに登録(10〜30分)HPA → 自動でPod追加(数秒〜数十秒)
デプロイ手動でブルーグリーン/カナリア切替。LBの重み変更RollingUpdate戦略でPodを段階的に入れ替え。rollout undoでロールバック
バックアップVSSスナップショット / テープバックアップCronJob + mysqldump。PVCスナップショット(CSIドライバー依存)

大きな違いは「自動化のレイヤーが標準で存在する」ことです。VMの監視設計では「異常を検知したら誰に連絡するか」が主題でしたが、K8sの監視設計では「Probeの設定で自動復旧がどこまでカバーされるか、カバーされない範囲を誰がどう対処するか」が主題になります。K8sの運用設計は、自動化と人間の対応の境界を明確にする作業とも言えます。

7.2.3 「構築前の設計」と「構築後の運用設計」の違い

第1〜3回の設計フェーズでは、白紙の状態から設計書を作りました。要件を分析し、リソースを選定し、パラメータを決定する。まだシステムは動いていないため、設計値の根拠は「ソフトウェアの仕様」「ベストプラクティス」「応用編での実測経験」でした。

本回の運用設計は、動いているシステムが前提です。kubectl topで実際のリソース消費を観察し、負荷テストで実際のスケーリング挙動を確認し、バックアップからのリストアを実際にテストする。「実データに基づく設計」ができるのが、構築後の運用設計の強みです。

VMの世界でも同じです。構築前の設計書はベンダー推奨値やサイジングシートに基づきますが、運用設計書は本番稼働後のモニタリングデータに基づいて閾値やスケーリング基準を調整します。構築前の設計が「推定」なら、構築後の運用設計は「実測」に基づく設計です。

7.3 監視設計 — 「異常をどう検知するか」を決める

監視設計では、「何を監視するか」「閾値は何か」「異常時にどうするか」を決めます。まずAIを使って監視項目一覧のたたき台を作成し、その後に実機確認で検証・修正していきます。

7.3.1 監視項目一覧を作成する

監視項目一覧は運用設計書の中核です。TaskBoardの全コンポーネントについて、何を監視し、どの閾値で異常と判定し、異常時にどうアクションするかを一覧化します。

最初のたたき台をAIで生成し、実データで検証するアプローチを取ります。

💬 あなた → AI(Claude):
TaskBoardの監視項目一覧のドラフトを作ってほしい。
構成は Payara Micro(APIサーバー)+ MySQL(DB)+ Nginx(フロントエンド)の3コンポーネント。
Kubernetesで運用する前提で、各コンポーネントの監視対象、閾値、異常時アクションを表形式で整理してほしい。

🤖 Claude の回答:

TaskBoardの監視項目一覧を作成します。

コンポーネント監視対象閾値異常時アクション
Payara MicroCPU使用率80%以上が5分継続HPAによる自動スケールアウト / 手動調査
メモリ使用率90%以上OOMKillの兆候。JVMヒープ設定を確認
HTTPレスポンスコード5xx率 > 1%Pod状態確認、ログ確認
MySQLCPU使用率70%以上が5分継続スロークエリ調査
メモリ使用率85%以上innodb_buffer_pool_sizeの見直し
ディスク使用率80%以上PVC拡張またはデータアーカイブ
NginxCPU使用率80%以上が5分継続HPAによる自動スケールアウト
メモリ使用率90%以上worker_connectionsの見直し

🔍 ここで立ち止まって確認しましょう

AIの出力は「K8sで動く一般的なWebアプリ」としてはおおむね妥当です。しかし、TaskBoard固有の考慮が不足しています。以下の観点で修正が必要です。

Probeの監視項目が抜けている — K8sの監視設計で最も重要なのはProbeです。liveness/readinessの失敗回数やPodの再起動回数は、AIのドラフトに含まれていません
Payara Microの起動時間(15-20秒)が未考慮 — startupProbeがないとCrashLoopBackOffに陥る体験を応用編第8回でしました。監視設計にstartupProbeの状態を含めるべきです
MicroProfile Healthのエンドポイントが明記されていない — Payara MicroはMicroProfile Health標準の/health/live/health/ready/health/startedを使います。汎用のHTTPチェックではなく、これらの具体的なエンドポイントを明記すべきです
kind環境の制約が未反映 — kind環境ではPrometheus/Grafanaは導入していません。監視手段はkubectl topとProbeに限定されます。本番環境での監視ツール導入は第10回で扱います
CPU/メモリの閾値に実測根拠がない — 「80%」「90%」は一般的な数値ですが、TaskBoardの定常値をkubectl topで実測してから閾値を設定すべきです

AIのドラフトをたたき台として活用しつつ、応用編で培った知識と実機確認で補完する。これが実務的なAI活用の姿勢です。まず実機でメトリクスを確認し、そのデータを基に監視項目一覧を修正していきます。

7.3.2 Probeによる自動復旧の仕組みを整理する

K8sの監視設計でまず整理すべきは、Probeによる自動復旧の範囲です。第3回(詳細設計)で設計し、第5回(アプリケーション構築)で適用したProbe設定を、運用観点で確認します。

Probeの現在の設定を確認しましょう。

[Execution User: developer]

# TaskBoard API(Payara Micro)のProbe設定を確認
kubectl get deployment taskboard-api -n app -o jsonpath='{.spec.template.spec.containers[0]}' | python3 -m json.tool | grep -A 10 'Probe'

設定値を読み取りやすくするため、kubectl describeも併用します。

[Execution User: developer]

kubectl describe deployment taskboard-api -n app | grep -A 5 'Liveness\|Readiness\|Startup'
    Liveness:   http-get http://:8080/health/live delay=0s timeout=3s period=10s #success=1 #failure=3
    Readiness:  http-get http://:8080/health/ready delay=0s timeout=3s period=5s #success=1 #failure=3
    Startup:    http-get http://:8080/health/started delay=0s timeout=3s period=2s #success=1 #failure=30

同様にNginxとMySQLも確認します。

[Execution User: developer]

kubectl describe deployment nginx -n app | grep -A 5 'Liveness\|Readiness\|Startup'
    Liveness:   http-get http://:8080/ delay=0s timeout=3s period=10s #success=1 #failure=3
    Readiness:  http-get http://:8080/ delay=0s timeout=3s period=5s #success=1 #failure=3

[Execution User: developer]

kubectl describe statefulset mysql -n db | grep -A 5 'Liveness\|Readiness\|Startup'
    Liveness:   tcp-socket :3306 delay=15s timeout=3s period=10s #success=1 #failure=3
    Readiness:  tcp-socket :3306 delay=0s timeout=3s period=5s #success=1 #failure=3

Probeの自動復旧をまとめます。これはK8sが「自動で」対処してくれる範囲です。

コンポーネントProbe失敗時のアクション検知までの最大時間
TaskBoard APIstartupProbe(/health/started)起動完了までliveness/readinessを抑制。最大60秒(2秒×30回)で起動完了を待つ60秒
livenessProbe(/health/live)30秒間の無応答でコンテナ再起動(10秒×3回)30秒
readinessProbe(/health/ready)15秒間の無応答でServiceエンドポイントから除外(5秒×3回)。DB接続確認を含む15秒
NginxlivenessProbe(HTTP GET /)30秒間の無応答でコンテナ再起動30秒
readinessProbe(HTTP GET /)15秒間の無応答でServiceエンドポイントから除外15秒
MySQLlivenessProbe(TCP 3306)初期待機15秒後、30秒間の無応答でコンテナ再起動45秒
readinessProbe(TCP 3306)15秒間の無応答でHeadless Serviceから除外15秒

重要なのは「Probeでカバーされない異常」を把握しておくことです。Probeは「プロセスが応答するか」を判定しますが、「レスポンスが遅い」「データが破損している」「ディスクが逼迫している」といった異常は検知できません。これらはkubectl topやログ確認など、人間が能動的に監視する必要があります。

7.3.3 kubectl topでリソースの定常値を観察する

監視閾値を決めるには、まず「正常な状態」のメトリクスを知る必要があります。VMの世界でもZabbixの導入直後に1〜2週間のベースラインを取得するのが定石でした。K8sでも同じです。

まずNode全体のリソース消費を確認します。

[Execution User: developer]

kubectl top nodes
NAME                       CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
taskboard-control-plane    180m         9%     1100Mi          28%
taskboard-worker           250m         12%    1400Mi          36%
taskboard-worker2          230m         11%    1350Mi          34%
taskboard-worker3          220m         11%    1300Mi          33%

Worker Node全体でCPU使用率は11〜12%、メモリ使用率は33〜36%程度です。kindクラスタのリソースには十分な余裕があります。

次に、app NamespaceのPodごとのリソース消費を確認します。

[Execution User: developer]

kubectl top pods -n app
NAME                              CPU(cores)   MEMORY(bytes)
nginx-6d4f5b7c8-abcde            2m           25Mi
nginx-6d4f5b7c8-fghij            2m           24Mi
taskboard-api-7b8c9d0e1-klmno    15m          320Mi
taskboard-api-7b8c9d0e1-pqrst    18m          315Mi

db NamespaceのPodも確認します。

[Execution User: developer]

kubectl top pods -n db
NAME        CPU(cores)   MEMORY(bytes)
mysql-0     8m           280Mi

定常時のリソース消費が明らかになりました。これを表にまとめます。

コンポーネントCPU定常値requests.cpu利用率メモリ定常値requests.memory利用率
Nginx2m50m4%25Mi64Mi39%
TaskBoard API15-18m200m8-9%315-320Mi384Mi82-83%
MySQL8m200m4%280Mi256Mi109%

注目すべき点がいくつかあります。まず、NginxのCPU利用率はわずか4%です。静的ファイル配信が中心のため、当然の結果です。TaskBoard APIのメモリ利用率は82-83%で、requests(384Mi)に対して定常消費が315-320Miです。第3回で設計した通り、JVMのヒープ領域が安定的にメモリを消費しています。MySQLのメモリはrequests(256Mi)を超えて280Miを使用していますが、limits(512Mi)には余裕があります。requestsはSchedulerの配置判断に使う「予約量」であり、実消費がrequestsを超えることは問題ありません。limitsを超えた場合にOOMKillが発生します。

この実測データを基に、監視閾値を設定します。一般に、定常値の2〜3倍をアラート閾値の目安にします。定常値から大きく外れた状態が継続する場合に、何らかの異常が発生している可能性が高いためです。

7.3.4 監視設計のまとめ

AIのドラフトと実機確認の結果を統合し、TaskBoardの監視設計を完成させます。

運用設計書 — 1. 監視設計

1.1 監視項目一覧

コンポーネント監視対象定常値(実測)閾値監視手段異常時アクション
TaskBoard APICPU使用率15-18m(requestsの8-9%)CPU requestsの80%(160m)超過が5分継続kubectl top / HPAHPAが自動スケールアウト。スケール上限到達時は手動調査
メモリ使用量315-320Mi450Mi超過(limitsの88%)kubectl topJVMヒープダンプ取得、メモリリーク調査
Pod再起動回数01回以上/時kubectl get pods(RESTARTS列)livenessProbe失敗の原因を調査。ログ確認
readinessProbe失敗0継続的な失敗kubectl describe pod(Events)DB接続の確認。MySQL Podの状態確認
MySQLCPU使用率8m(requestsの4%)CPU requestsの70%(140m)超過が5分継続kubectl topスロークエリの調査
メモリ使用量280Mi430Mi超過(limitsの84%)kubectl top接続数・innodb_buffer_pool使用状況の確認
Pod再起動回数01回以上/時kubectl get pods(RESTARTS列)データ整合性確認。バックアップからの復元を検討
NginxCPU使用率2m(requestsの4%)CPU requestsの80%(40m)超過が5分継続kubectl top / HPAHPAが自動スケールアウト
Pod再起動回数01回以上/時kubectl get pods(RESTARTS列)設定ファイルの確認。ConfigMapの検証

1.2 Probeによる自動復旧設計

7.3.2節で整理した通り。K8sのProbeが自動で対処する範囲と、人間が対処すべき範囲を明確に区分する。

Probeが自動で対処する異常:プロセスハング(liveness)、一時的なサービス不能(readiness)、起動遅延(startup)。

人間が対処すべき異常:パフォーマンス劣化(レスポンス遅延)、データ不整合、リソース逼迫の傾向、設定ミスによる動作異常。

1.3 リソース監視(CPU/メモリの定常値と閾値)

7.3.3節の実測データに基づく。定常値は環境やワークロードの変化に応じて更新する。本番運用開始後、1〜2週間のベースライン取得期間を設け、閾値を再調整する。

kind環境での監視の限界: 本環境ではkubectl topとProbeによる監視が中心です。本番環境ではPrometheus + Grafanaの導入により、メトリクスの時系列保存、ダッシュボード可視化、アラート通知(Alertmanager)が可能になります。監視ツールの本格導入は第10回で扱います。

7.4 スケーリング設計 — 「負荷増にどう対応するか」を決める

7.4.1 HPA設定の根拠を検証する

第3回(詳細設計)でHPAのパラメータを設計し、第5回で適用しました。ここで改めて設定値を確認し、運用設計書として根拠を文書化します。

[Execution User: developer]

kubectl get hpa -n app
NAME                REFERENCE                     TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa           Deployment/nginx               4%/70%    2         6         2          3d
taskboard-api-hpa   Deployment/taskboard-api       8%/70%    2         4         2          3d

定常時のCPU使用率はNginxが4%、TaskBoard APIが8%で、閾値の70%に対して十分な余裕があります。ここで、HPA設計の根拠を整理します。

averageUtilization: 70%の根拠: CPU requestsに対する使用率の平均が70%を超えたらスケールアウトします。70%という値は「スケールアウト開始後、新しいPodが起動してトラフィックを受け始めるまでの間に、既存Podが100%に到達しないための余裕」を見込んだ値です。Payara Microの起動時間(15-20秒)を考慮すると、70%から100%に達するまでの間に新しいPodが起動を完了する必要があります。もし閾値を90%にすると、スケールアウトが開始されてから新しいPodが起動を完了するまでの間に既存PodのCPUが飽和する可能性があります。

Nginx — minReplicas: 2 / maxReplicas: 6: minReplicas: 2は可用性確保のための最小値です。PDB(minAvailable: 1)との組み合わせで、1 Podの障害やdrainに耐えられます。maxReplicas: 6は、Nginx 6 Pod × requests 50m = 300mで、app NamespaceのResourceQuota(requests.cpu: 2コア = 2000m)に対して15%です。TaskBoard APIとの共存を考慮しても十分な余裕があります。

TaskBoard API — minReplicas: 2 / maxReplicas: 4: minReplicas: 2はNginxと同じ理由です。maxReplicas: 4は、TaskBoard API 4 Pod × requests 200m = 800mで、ResourceQuota(2000m)の40%です。JVMベースのPayara Microはメモリ消費が大きいため(requests 384Mi × 4 = 1536Mi、Quota 2Gi)、メモリ面でも4 Podが妥当な上限です。Nginxよりmaxが小さいのは、1 Podあたりのリソース消費が大きいためです。

TaskBoard API — behavior.scaleUp.policies(1 Podずつ / 60秒間隔): Payara Microは起動に15-20秒かかります。一度に複数Podを起動するとNode上のCPUが一時的に跳ね上がるため、1 Podずつ慎重にスケールアウトします。60秒のperiodSecondsは、前のPodが起動を完了してトラフィックを受け始めるのを待ってから次のPodを追加する余裕です。

7.4.2 負荷テストでスケールアウト/スケールインを確認する

HPA設計の根拠を実機で検証します。応用編第8回と同様にstress-ngで負荷をかけ、スケールアウトとスケールインの挙動を観察します。

ターミナルを2つ用意します。ターミナル1でHPAの状態をリアルタイム監視します。

[ターミナル1 — Execution User: developer]

kubectl get hpa -n app --watch

ターミナル2でNginxに対してHTTPリクエストの負荷をかけます。一時的なPodからwgetを連続実行して、CPUに負荷をかけます。

[ターミナル2 — Execution User: developer]

# Nginx Serviceに対してHTTPリクエストの負荷をかける
kubectl run load-test -n app --rm -it --restart=Never \
  --image=busybox:1.36 -- /bin/sh -c \
  "while true; do wget -q -O- http://nginx.app.svc.cluster.local:8080/ > /dev/null 2>&1; done"

1〜2分後、ターミナル1でHPAの変化を観察します。

NAME        REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa   Deployment/nginx    4%/70%    2         6         2          3d
nginx-hpa   Deployment/nginx    48%/70%   2         6         2          3d1m
nginx-hpa   Deployment/nginx    85%/70%   2         6         2          3d2m
nginx-hpa   Deployment/nginx    85%/70%   2         6         3          3d2m30s
nginx-hpa   Deployment/nginx    62%/70%   2         6         3          3d3m

CPU使用率が70%を超えた時点でREPLICASが2から3に増えています。3 Podに分散されたことでCPU使用率が62%に低下しました。これがHPAの正常動作です。

負荷を停止します。ターミナル2でCtrl+Cを押してPodを終了させてください。その後、ターミナル1でスケールインの挙動を観察します。

nginx-hpa   Deployment/nginx    62%/70%   2         6         3          3d3m
nginx-hpa   Deployment/nginx    20%/70%   2         6         3          3d4m
nginx-hpa   Deployment/nginx    3%/70%    2         6         3          3d5m
nginx-hpa   Deployment/nginx    3%/70%    2         6         3          3d8m
nginx-hpa   Deployment/nginx    3%/70%    2         6         2          3d10m

負荷が下がっても即座にはPodが減りません。応用編第8回で設定したscaleDown.stabilizationWindowSeconds: 300(5分間)の安定化ウィンドウが効いています。一時的な負荷低下でPodが減らされ、すぐに負荷が戻ってまた増やすという「フラッピング」を防ぐための仕組みです。5分間安定してから段階的にスケールインしています。

7.4.3 スケーリングの上限 — ResourceQuotaとの関係

HPAはPodを無制限に増やせるわけではありません。maxReplicasのほかに、ResourceQuotaという「天井」が存在します。応用編第1回で設定し、第8回でも触れたResourceQuotaとHPAの関係を、運用設計の観点で整理します。

app NamespaceのResourceQuotaは以下の通りです。

requests.cpu: "2"          # CPU requestsの合計上限: 2コア
requests.memory: "2Gi"     # メモリrequestsの合計上限: 2GiB
limits.cpu: "4"            # CPU limitsの合計上限: 4コア
limits.memory: "4Gi"       # メモリlimitsの合計上限: 4GiB
pods: "20"                 # Pod数の上限

全Podがフルスケールアウトした場合のリソース消費を計算します。

コンポーネントPod数CPU requests合計メモリ requests合計
Nginx(max 6 Pod)650m × 6 = 300m64Mi × 6 = 384Mi
TaskBoard API(max 4 Pod)4200m × 4 = 800m384Mi × 4 = 1536Mi
合計101100m1920Mi
Quota上限202000m2048Mi(2Gi)
余裕10900m128Mi

CPU requests合計は1100m / 2000mで55%。十分な余裕があります。メモリ requests合計は1920Mi / 2048Miで94%。フルスケールアウト時にはメモリがほぼQuota上限に達します。仮にNginxが6 Pod、TaskBoard APIが4 Podの状態でさらに負荷テスト用のPodを起動しようとすると、Quotaに引っかかる可能性があります。

運用設計書にはこの上限を明記し、「フルスケールアウト状態でメモリQuotaの余裕が128Miしかない」という制約を把握しておきます。本番環境ではResourceQuotaの値を調整するか、maxReplicasを抑えることで対処します。

7.4.4 スケーリング設計のまとめ

運用設計書 — 2. スケーリング設計

2.1 HPA設定

パラメータNginx HPATaskBoard API HPA設計根拠
minReplicas22可用性確保。PDB(minAvailable: 1)との組み合わせで1 Pod障害に耐える
maxReplicas64ResourceQuota(requests.cpu: 2コア)との関係。APIはJVMのメモリ消費大のため控えめ
averageUtilization70%70%新Pod起動(APIは15-20秒)までに既存Podが飽和しない余裕
scaleDown.stabilizationWindowSeconds300300フラッピング防止。5分間の安定確認後にスケールイン
scaleUp.policies(APIのみ)1 Pod / 60秒JVMの起動時CPU消費を考慮。慎重なスケールアウト

2.2 スケーリングの上限(ResourceQuotaとの関係)

フルスケールアウト時(Nginx 6 + API 4 = 10 Pod)のリソース消費: CPU requests合計1100m / Quota 2000m(55%)、メモリ requests合計1920Mi / Quota 2Gi(94%)。メモリは上限に近いため、フルスケールアウト状態での追加Pod起動に注意。

2.3 負荷テスト結果

7.4.2節の負荷テストにより以下を確認済み。スケールアウト: CPU使用率70%超過後、30秒以内にPod追加を確認。スケールイン: 負荷停止後、5分間の安定化ウィンドウ経過後に段階的にPod削減を確認。

7.5 デプロイ戦略 — 「更新をどう安全に行うか」を決める

7.5.1 ローリングアップデートのパラメータ設計

VMの世界でのアプリケーション更新を思い出してください。ブルーグリーンデプロイなら新旧2系統のVMを用意してLBで切り替え、カナリアデプロイなら一部のトラフィックだけ新系統に流す。いずれも手動でのLB設定変更が必要でした。

K8sのDeploymentはRollingUpdate戦略を標準で備えています。古いPodと新しいPodを段階的に入れ替え、入れ替え中もサービスの可用性を維持します。この入れ替えの「速度」と「安全性」を制御するのが、maxSurgemaxUnavailableです。

パラメータ意味値が大きいと値が小さいと
maxSurgereplicas数を超えて追加できるPodの最大数更新が速い(リソース消費が増える)更新が遅い(リソース消費が抑えられる)
maxUnavailable利用不可にできるPodの最大数更新が速い(一時的な容量低下)更新が遅い(容量を維持)

第3回(詳細設計)で、NginxとTaskBoard APIの両方にmaxSurge: 1, maxUnavailable: 0を設計しました。この組み合わせの意味を改めて整理します。

maxUnavailable: 0は「アップデート中に利用不可のPodを0にする」、つまり常にすべてのPodがリクエストを処理できる状態を維持します。maxSurge: 1は「新しいPodを1つ先に起動する」ことを意味します。新しいPodが起動しReadyになった後に、古いPodが1つ停止します。

replicas: 2の場合の入れ替わりの流れは次の通りです。

初期状態:     [旧Pod-A] [旧Pod-B]              → 2 Pod稼働
Step 1:       [旧Pod-A] [旧Pod-B] [新Pod-A]    → 3 Pod(maxSurge: 1で1つ追加)
              新Pod-AがReady判定されるのを待つ
Step 2:       [旧Pod-B] [新Pod-A]              → 旧Pod-Aを停止。2 Pod稼働
Step 3:       [旧Pod-B] [新Pod-A] [新Pod-B]    → 新Pod-Bを追加。3 Pod
Step 4:       [新Pod-A] [新Pod-B]              → 旧Pod-Bを停止。2 Pod稼働

アップデート中のどの時点でも、最低2 PodがReady状態でリクエストを処理しています。サービス断は発生しません。

7.5.2 Payara Microの起動時間を考慮したmaxSurge/maxUnavailable

もし逆の組み合わせ——maxSurge: 0, maxUnavailable: 1——にした場合はどうなるでしょうか。

初期状態:     [旧Pod-A] [旧Pod-B]              → 2 Pod稼働
Step 1:       [旧Pod-B]                        → 旧Pod-Aをまず停止。1 Pod稼働
              新Pod-Aの起動を待つ(Payara Microは15-20秒)
Step 2:       [旧Pod-B] [新Pod-A]              → 新Pod-AがReady。2 Pod稼働
Step 3:       [新Pod-A]                        → 旧Pod-Bを停止。1 Pod稼働
              新Pod-Bの起動を待つ
Step 4:       [新Pod-A] [新Pod-B]              → 2 Pod稼働

Step 1とStep 3で、1 Podしか稼働していない期間が15-20秒間発生します。Payara Microの起動時間が長いため、この期間はサービスの処理能力が半減します。Nginxであれば起動は1秒以内ですが、JVMベースのPayara Microではこの差が運用に影響します。

これが、第3回でmaxSurge: 1, maxUnavailable: 0を選んだ理由です。「新しいPodを先に起動してから古いPodを停止する」ことで、アップデート中の容量低下を防ぎます。リソース消費は一時的に増えますが、ResourceQuotaに十分な余裕があるため問題ありません。

実際にローリングアップデートの挙動を確認します。TaskBoard APIのイメージタグは実際には変更しませんが、kubectl rollout restartで同じ効果を得られます。

[Execution User: developer]

# ローリングアップデートを開始
kubectl rollout restart deployment/taskboard-api -n app
deployment.apps/taskboard-api restarted

別のターミナルでrollout statusを使ってアップデートの進行を観察します。

[Execution User: developer]

kubectl rollout status deployment/taskboard-api -n app
Waiting for deployment "taskboard-api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "taskboard-api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "taskboard-api" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "taskboard-api" rollout to finish: 1 old replicas are pending termination...
deployment "taskboard-api" successfully rolled out

新しいPodが起動し、startupProbeで起動完了が確認されてから古いPodが停止する、という流れが確認できます。Payara Microの起動時間(15-20秒)を加味すると、2 Pod × 2回の入れ替えで全体のアップデート完了まで40-50秒程度です。

次に、ロールバックを試します。

[Execution User: developer]

# デプロイ履歴を確認
kubectl rollout history deployment/taskboard-api -n app
deployment.apps/taskboard-api
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

[Execution User: developer]

# 1つ前のリビジョンにロールバック
kubectl rollout undo deployment/taskboard-api -n app
deployment.apps/taskboard-api rolled back

[Execution User: developer]

# ロールバック完了を確認
kubectl rollout status deployment/taskboard-api -n app
deployment "taskboard-api" successfully rolled out

ロールバックもローリングアップデートと同じ仕組みで行われます。maxSurge: 1, maxUnavailable: 0の設定により、ロールバック中もサービス断は発生しません。

7.5.3 デプロイ手順とロールバック手順

運用設計書には、デプロイとロールバックの手順を具体的に記載します。「手順書通りにやれば間違えない」レベルの具体性が求められます。

デプロイ手順(TaskBoard APIの場合):

# 1. 変更をドライランで確認(サーバーサイド)
kubectl apply -f deployment-taskboard-api.yaml --dry-run=server

# 2. 現在の設定との差分を確認
kubectl diff -f deployment-taskboard-api.yaml

# 3. 適用
kubectl apply -f deployment-taskboard-api.yaml

# 4. ローリングアップデートの進行を監視
kubectl rollout status deployment/taskboard-api -n app

# 5. 新しいPodの状態を確認
kubectl get pods -n app -l component=api

# 6. 動作確認(HealthCheckとAPI応答)
kubectl exec -n app deploy/taskboard-api -- \
  curl -s http://localhost:8080/health/ready
kubectl exec -n app deploy/taskboard-api -- \
  curl -s http://localhost:8080/api/tasks

ロールバック手順:

# 1. 問題を検知(5xxエラー、Pod再起動、HealthCheck失敗等)

# 2. ロールバック実行
kubectl rollout undo deployment/taskboard-api -n app

# 3. ロールバック完了を確認
kubectl rollout status deployment/taskboard-api -n app

# 4. 旧バージョンで正常動作していることを確認
kubectl exec -n app deploy/taskboard-api -- \
  curl -s http://localhost:8080/health/ready

ロールバック判断基準: デプロイ後5分以内に以下のいずれかを検知した場合、即座にロールバックを実施します。PodのCrashLoopBackOff、readinessProbeの継続的な失敗、API応答のHTTP 5xxエラー率上昇、rollout statusがタイムアウト(Podが起動しない)。

7.5.4 デプロイ戦略のまとめ

運用設計書 — 3. デプロイ戦略

3.1 ローリングアップデート設定

コンポーネントstrategymaxSurgemaxUnavailable設計根拠
NginxRollingUpdate10起動1秒以内。新Podを先に起動して容量維持
TaskBoard APIRollingUpdate10起動15-20秒。maxUnavailable: 0で容量維持が必須
MySQLRollingUpdate(StatefulSetデフォルト)replicas: 1のため設定による差なし。アップデート時は一時停止が発生

3.2 デプロイ手順

dry-run → diff → apply → rollout status → 動作確認 の5ステップ。7.5.3節に詳細手順を記載。

3.3 ロールバック手順

kubectl rollout undoによる即時ロールバック。判断基準: デプロイ後5分以内のCrashLoopBackOff、readiness失敗、5xxエラー率上昇。Helmを使用している場合はhelm rollbackを使用(第8回で扱う)。

7.6 バックアップ設計 — 「データをどう守るか」を決める

VMの世界でバックアップ設計書を書いたことのある方なら、「リストアできないバックアップは無意味」という原則をご存知でしょう。テープにデータを書き出したつもりが、いざリストアしようとしたらテープが読めなかった——そんな事故を防ぐために、バックアップ設計にはリストアテストが必須です。K8sでも同じです。

7.6.1 バックアップスケジュールと保持ポリシー

応用編第4回で作成したDBバックアップCronJobの現在の設定を確認します。

[Execution User: developer]

kubectl get cronjob db-backup -n db -o yaml | grep -A 5 'schedule\|successfulJobsHistoryLimit\|failedJobsHistoryLimit'
  schedule: '*/1 * * * *'
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

現在のスケジュールは*/1 * * * *(毎分実行)です。これは応用編第4回と第5回で動作確認のために設定したハンズオン用の値です。本番運用では適切なスケジュールに変更する必要があります。

バックアップスケジュールの設計にはRPO(Recovery Point Objective: 目標復旧時点)の考え方が必要です。RPOはデータ損失をどの程度許容するかを示します。RPOが1時間なら「障害発生時に最大1時間分のデータが失われることを許容する」、RPOが24時間なら「最大24時間分のデータ損失を許容する」ということです。

TaskBoardは社内タスク管理アプリケーションです。リアルタイムのトランザクションを処理する決済システムではありません。RPOを6時間と設定し、バックアップスケジュールを6時間ごとに設計します。

項目設計値根拠
バックアップスケジュール0 */6 * * *(6時間ごと: 0:00, 6:00, 12:00, 18:00)RPO 6時間。社内タスク管理アプリの要件に適合
成功履歴保持数successfulJobsHistoryLimit: 5直近5世代(30時間分)のJob履歴を保持。障害調査時にログを追跡可能
失敗履歴保持数failedJobsHistoryLimit: 3失敗Jobは原因調査のため3世代保持
バックアップ方式mysqldump –single-transactionInnoDB環境でロックなしの論理バックアップ
バックアップ保存先Pod内の/tmp(ログ出力で確認)kind環境の制約。本番ではS3/NFS等の外部ストレージ

運用設計書にスケジュール変更のマニフェストを記載します。第8回(日常運用)で実際に変更を適用します。

# 運用設計書記載のバックアップスケジュール(変更後)
# schedule: "*/1 * * * *"  ← ハンズオン用(毎分)
schedule: "0 */6 * * *"    # ← 本番用(6時間ごと)
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 3

kind環境でのバックアップの限界: 現在のCronJobではバックアップデータをPod内の/tmpに出力し、Job完了時にPodとともに消えます。つまり実質的には「バックアップの実行確認」のみ行っている状態です。本番環境ではPVCへの保存やS3/GCSへのアップロードが必要です。本回のリストアテストでは、バックアップJobを手動実行し、Job実行中(Podが生きている間)にバックアップデータを取得してリストアに使用します。

7.6.2 リストア手順を設計する

リストア手順は、障害発生時に冷静に実行できるレベルの具体性が必要です。深夜3時にDB障害が発生したとき、手順書を見ながら確実に復旧できることが目標です。

TaskBoardのDBリストア手順を設計します。

# TaskBoard DBリストア手順

# 前提: バックアップファイル(backup.sql)が手元にあること

# 1. 現在のDB状態を確認
kubectl exec -n db mysql-0 -- mysql -u root -p"${MYSQL_ROOT_PASSWORD}" \
  -e "SELECT COUNT(*) FROM taskboard.tasks;"

# 2. TaskBoard APIの接続を一時停止(任意: メンテナンス中のデータ変更を防止)
kubectl scale deployment taskboard-api -n app --replicas=0

# 3. バックアップファイルをMySQL Podにコピー
kubectl cp backup.sql db/mysql-0:/tmp/backup.sql

# 4. リストアを実行
kubectl exec -n db mysql-0 -- mysql -u root -p"${MYSQL_ROOT_PASSWORD}" \
  taskboard < /tmp/backup.sql

# 5. リストア結果を確認
kubectl exec -n db mysql-0 -- mysql -u root -p"${MYSQL_ROOT_PASSWORD}" \
  -e "SELECT COUNT(*) FROM taskboard.tasks;"

# 6. TaskBoard APIを復旧
kubectl scale deployment taskboard-api -n app --replicas=2

# 7. API動作確認
kubectl exec -n app deploy/taskboard-api -- \
  curl -s http://localhost:8080/api/tasks

7.6.3 リストアテストを実施する

設計したリストア手順が実際に動作することを検証します。バックアップを取得し、データを変更し、バックアップからリストアしてデータが復元されることを確認します。

まず、現在のタスクデータを確認します。

[Execution User: developer]

# 現在のタスク数を確認
kubectl exec -n db mysql-0 -- mysql -u root -p"$(kubectl get secret mysql-secret -n db -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d)" \
  -e "SELECT COUNT(*) AS task_count FROM taskboard.tasks;" 2>/dev/null
+------------+
| task_count |
+------------+
|          5 |
+------------+

DB初期化Jobで投入した5件のタスクが存在しています。この状態でバックアップを取得します。

[Execution User: developer]

# CronJobから手動でバックアップJobを実行
kubectl create job --from=cronjob/db-backup manual-backup-test -n db
job.batch/manual-backup-test created

[Execution User: developer]

# Jobの完了を待つ
kubectl wait --for=condition=complete job/manual-backup-test -n db --timeout=60s
job.batch/manual-backup-test condition met

バックアップが成功しました。Jobのログからバックアップ内容を確認し、バックアップデータを取得します。

[Execution User: developer]

# バックアップJobのPod名を取得し、SQLダンプを取得
BACKUP_POD=$(kubectl get pods -n db -l job-name=manual-backup-test -o jsonpath='{.items[0].metadata.name}')
kubectl cp db/${BACKUP_POD}:/tmp/ /home/developer/backup-files/ 2>/dev/null

# バックアップファイルを確認
ls -lh ~/backup-files/backup-*.sql
-rw-r--r-- 1 developer developer 3.2K Feb 17 10:15 /home/developer/backup-files/backup-20260217-101500.sql

バックアップファイルが取得できました。次に、リストアテスト用のデータ変更を行います。APIを通じてタスクを追加し、その後バックアップからリストアして「追加したタスクが消え、バックアップ時点の状態に戻ること」を確認します。

[Execution User: developer]

# テストタスクを3件追加
for i in 1 2 3; do
  kubectl exec -n app deploy/taskboard-api -- \
    curl -s -X POST http://localhost:8080/api/tasks \
    -H "Content-Type: application/json" \
    -d "{\"title\": \"Restore Test Task ${i}\", \"description\": \"This should disappear after restore\"}"
done

# タスク数を確認(5 + 3 = 8件のはず)
kubectl exec -n db mysql-0 -- mysql -u root -p"$(kubectl get secret mysql-secret -n db -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d)" \
  -e "SELECT COUNT(*) AS task_count FROM taskboard.tasks;" 2>/dev/null
+------------+
| task_count |
+------------+
|          8 |
+------------+

8件に増えました。ここからバックアップ時点(5件)の状態にリストアします。

[Execution User: developer]

# APIを一時停止してリストア中のデータ変更を防止
kubectl scale deployment taskboard-api -n app --replicas=0
kubectl wait --for=delete pod -l component=api -n app --timeout=60s 2>/dev/null || true

[Execution User: developer]

# バックアップファイルをMySQL Podにコピー
BACKUP_FILE=$(ls ~/backup-files/backup-*.sql | head -1)
kubectl cp ${BACKUP_FILE} db/mysql-0:/tmp/restore.sql

# リストアを実行
kubectl exec -n db mysql-0 -- bash -c \
  'mysql -u root -p"${MYSQL_ROOT_PASSWORD}" taskboard < /tmp/restore.sql'

[Execution User: developer]

# リストア結果を確認
kubectl exec -n db mysql-0 -- mysql -u root -p"$(kubectl get secret mysql-secret -n db -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d)" \
  -e "SELECT COUNT(*) AS task_count FROM taskboard.tasks;" 2>/dev/null
+------------+
| task_count |
+------------+
|          5 |
+------------+

8件から5件に戻りました。バックアップ時点の状態に正しくリストアされています。「Restore Test Task 1/2/3」は消え、DB初期化Jobで投入した5件のみが残っています。

[Execution User: developer]

# TaskBoard APIを復旧
kubectl scale deployment taskboard-api -n app --replicas=2
kubectl wait --for=condition=available deployment/taskboard-api -n app --timeout=120s
deployment.apps/taskboard-api condition met

[Execution User: developer]

# API経由でタスク一覧を確認
kubectl exec -n app deploy/taskboard-api -- \
  curl -s http://localhost:8080/api/tasks | python3 -m json.tool | head -20

APIからも5件のタスクのみが返されることを確認できます。リストアテスト完了です。

[Execution User: developer]

# テスト用のJobを削除
kubectl delete job manual-backup-test -n db
rm -rf ~/backup-files/

7.6.4 バックアップ設計のまとめ

運用設計書 — 4. バックアップ設計

4.1 バックアップスケジュールと保持ポリシー

項目設計値根拠
RPO(目標復旧時点)6時間社内タスク管理アプリ。リアルタイム性の要件は低い
スケジュール0 */6 * * *RPOに対応。毎日4回(0:00, 6:00, 12:00, 18:00)
バックアップ方式mysqldump –single-transactionロックなしの論理バックアップ。InnoDB環境で一貫性を保証
成功履歴保持数5世代(30時間分)直近の障害調査に十分
失敗履歴保持数3世代失敗原因の調査用

4.2 バックアップ手順

CronJobによる自動実行。手動実行が必要な場合: kubectl create job --from=cronjob/db-backup manual-backup -n db

4.3 リストア手順

7.6.2節に詳細手順を記載。要約: API停止 → バックアップファイルをPodにコピー → mysqlコマンドでリストア → データ確認 → API復旧。

4.4 リストアテスト結果

7.6.3節で実施。手動バックアップ取得(5件)→ テストデータ追加(8件に増加)→ バックアップからリストア → 5件に復元を確認。リストア手順は正常に動作することを検証済み。

7.7 この回のまとめ

7.7.1 成果物の確認 — TaskBoardの運用設計書

本回で作成した運用設計書の全体構造を確認します。

設計領域主なパラメータ根拠の取得方法
1. 監視設計監視項目一覧(9項目)、Probe自動復旧範囲、リソース閾値kubectl topによる定常値の実測
2. スケーリング設計HPA閾値70%、min/maxReplicas、stabilizationWindowstress-ngによる負荷テスト、ResourceQuotaとの整合性計算
3. デプロイ戦略maxSurge: 1 / maxUnavailable: 0、5ステップデプロイ手順、ロールバック判断基準kubectl rolloutによるアップデート挙動の観察
4. バックアップ設計RPO 6時間、6時間ごとのmysqldump、5世代保持リストアテストによる手順の検証

各設計領域で「実機確認 → 設計値決定 → 設計書記載」のフローを繰り返しました。構築前の設計(第1〜3回)が「推定」に基づいていたのに対し、運用設計は「実測」に基づいています。この違いが、運用設計書の説得力を高めます。

7.7.2 運用設計のチェックリスト

運用設計書の完全性を確認するためのチェックリストです。

チェック項目確認内容状態
監視項目一覧全コンポーネントの監視対象、閾値、アクションが定義されているか
Probeの自動復旧範囲Probeが対処する範囲と人間が対処する範囲が明確に区分されているか
リソース定常値kubectl topで定常値を実測し、閾値の根拠にしているか
HPA設計根拠閾値、min/max、behavior各パラメータに根拠が添えられているか
スケーリング上限ResourceQuotaとの関係が計算・文書化されているか
負荷テストスケールアウト/スケールインの挙動を実機で検証したか
デプロイ手順dry-run → diff → apply → 確認 → ロールバックの手順が具体的か
maxSurge/maxUnavailable根拠Payara Microの起動時間(15-20秒)が考慮されているか
バックアップスケジュールRPOに基づいた設計になっているか
リストア手順深夜でも手順通りに実行できるレベルの具体性があるか
リストアテスト実際にバックアップからリストアして正常性を確認したか

7.7.3 次回予告 — 運用設計書に基づく日常運用の実務

運用設計書は「仕組みの設計」です。次回(第8回)は、この設計書に基づいて日常運用の実務を行います。

第8回で扱うのは、アプリケーション更新の実務フロー(ソース変更 → WARリビルド → イメージ再作成 → デプロイ)、ConfigMap/Secret変更の反映と再起動戦略、TaskBoard全体のHelm化、values.yamlによる環境差分管理、Node drain/cordonによるノードメンテナンスです。本回で設計したデプロイ手順やロールバック手順を、実際の変更シナリオで活用します。

また、バックアップCronJobのスケジュール変更(毎分 → 6時間ごと)も第8回で「変更管理」の実践として適用します。運用設計書で決めた値を、変更管理の手順に従って安全に適用する——設計と運用がつながる瞬間です。


AI活用 — 運用設計書のドラフト生成:対話ログ

本回では7.3.1節で、監視項目一覧のたたき台をAIに生成させ、批判的にレビューするプロセスを体験しました。

AIのドラフトは「K8sの一般論としては概ね正しい」出発点を提供してくれます。しかし、TaskBoard固有の考慮——Payara Microの起動時間、MicroProfile Healthのエンドポイント、kind環境の制約、Probeの自動復旧——はAIが自動的には反映できません。応用編で培った知識とkubectl topの実測データがあって初めて、ドラフトを「使える設計書」に磨き上げることができます。

運用設計書のような定型的な文書はAIのドラフト生成と相性が良い領域です。ただし、「AIの出力をたたき台にして、実データで裏付ける」という姿勢を常に忘れないでください。