【Kubernetes実践編 #08】日常運用 — 変更管理とメンテナンスの実務
8.1 はじめに
8.1.1 前回の振り返り — 運用設計書が手元にある状態
前回(第7回)で、TaskBoardの運用設計書を作成しました。監視設計、スケーリング設計、デプロイ戦略、バックアップ設計——4つの領域をカバーする文書が手元にあります。稼働中のTaskBoardのメトリクスを観察し、負荷テストで挙動を検証し、バックアップからのリストアをテストした上で、実データに基づいた運用設計書を完成させています。
[TaskBoard 完全稼働状態]
全コンポーネント稼働中(第6回構築フェーズ完了時と同一)
運用設計書 作成済み(監視設計、スケーリング設計、デプロイ戦略、バックアップ設計)
ローリングアップデートのテスト済み(第7回デプロイ戦略検証で実施)
リストアテスト済み(第7回バックアップ設計検証で実施)
[Helm状態]
応用編第9回でフロントエンド(Nginx)のみHelm化済み
TaskBoard全体のHelm化は未実施 ← 本回で完成させる
8.1.2 本回の問題提起 — 「日々のオペレーションをどう回すか」
運用設計書は「仕組みの設計」でした。HPAの閾値、バックアップのスケジュール、ローリングアップデートのパラメータ——これらは「こう動くべき」という青写真です。
しかし、日常運用で実際に発生するのは「変更」です。アプリのバグ修正をデプロイしたい。ConfigMapの設定値を変えたい。ノードにOSパッチを当てたい。これらは毎週のように起きる日常的な作業であり、手順が定まっていなければ、変更のたびに判断を迫られることになります。
VMの世界を思い出してください。変更管理手順書なしに本番環境を触るプロジェクトはありません。「パッチ適用手順書」「設定変更手順書」「リリース手順書」——変更の種類ごとに手順書があり、手順書に沿ってオペレーションを行い、結果を記録する。K8sの世界でも、この原則は変わりません。
8.1.3 本回のゴールと成果物
本回のゴールは2つあります。
1つ目は、変更の種類に応じた安全な手順を設計・実行し、変更管理手順書として文書化すること。2つ目は、TaskBoard全体をHelm化し、変更管理のツール基盤を完成させることです。
本回の成果物
├── 変更管理手順書
│ ├── 1. アプリケーション更新手順
│ ├── 2. 設定変更手順
│ ├── 3. Helmによる変更管理
│ └── 4. ノードメンテナンス手順
└── 完成版Helmチャート(TaskBoard全体)
├── values.yaml(デフォルト)
├── values-dev.yaml / values-prod.yaml
└── 全コンポーネントのテンプレート
本回は運用フェーズの最終回です。5つのテーマすべてにハンズオンがあります。手を動かしながら手順書を書き上げ、運用フェーズを完了しましょう。
8.2 VMの日常運用とK8sの日常運用
8.2.1 VMの世界での変更管理 — パッチ適用・テンプレート・vMotion
VMware環境での日常運用を振り返りましょう。変更管理には大きく3つのカテゴリがありました。
アプリケーション更新では、WARファイルやバイナリをSCPでサーバーに転送し、サービスを停止して配置し、再起動する。ブルーグリーンデプロイメントを採用している場合は、LBの重みを切り替えて新環境に流す。問題があれば旧環境に戻す。手順書には「サービス停止コマンド」「ファイル配置先パス」「起動確認手順」が1行ずつ記載されていました。
設定変更では、httpd.confやmy.cnfの値を変更し、サービスを再起動する。Ansibleを使っている場合は、group_vars の値を変更してPlaybookを再実行する。変更前にdiffを取り、変更後に動作確認する。
ホストメンテナンスでは、ESXiにパッチを適用するためにVMをvMotionで別ホストに移動し、ホストをメンテナンスモードにしてパッチを適用し、リブート後にメンテナンスモードを解除する。DRSが有効であれば、VMの移動は自動で行われました。
8.2.2 K8sの変更管理 — ローリングアップデート・Helm・drain
K8sの世界では、これら3つのカテゴリがそれぞれ異なるツールと手順に対応します。
| 変更の種類 | VMの世界 | K8sの世界 |
|---|---|---|
| アプリ更新 | WAR配置 → サービス再起動 / LB切替 | イメージ再ビルド → kubectl apply → ローリングアップデート |
| 設定変更 | 設定ファイル編集 → サービス再起動 / Ansible再実行 | ConfigMap/Secret更新 → rollout restart / 自動反映 |
| ホストメンテナンス | vMotion → ESXiパッチ → リブート → メンテナンスモード解除 | cordon → drain → 作業 → uncordon |
| 変更管理ツール | VMテンプレート + Ansible + Tower/AWX | Helmチャート + values.yaml + helm history |
| ロールバック | 旧環境への切り戻し / Playbookの前回変数で再実行 | kubectl rollout undo / helm rollback |
注目すべき違いは「ロールバックの速度」です。VMの世界ではLBの切り替えやスナップショットの復元に数分〜数十分かかりましたが、K8sではkubectl rollout undoで数十秒、helm rollbackでも1分以内にロールバックが完了します。この速度が、K8sの変更管理を「安全に挑戦できるもの」に変えています。
8.2.3 変更の種類と対応手順の分類
本回で扱う変更を分類します。これが変更管理手順書の骨格になります。
| 変更の種類 | 頻度 | リスク | 対応手順 |
|---|---|---|---|
| アプリケーション更新 | 週次〜月次 | 中(デグレの可能性) | §8.3 で扱う |
| ConfigMap / Secret変更 | 月次〜随時 | 低〜中(反映方法に注意) | §8.4 で扱う |
| Helm upgrade(パラメータ変更) | 随時 | 低(values.yamlの差分のみ) | §8.5〜8.6 で扱う |
| Nodeメンテナンス | 月次〜四半期 | 高(サービス影響の可能性) | §8.7 で扱う |
8.3 アプリケーション更新の実務フロー
8.3.1 TaskBoard APIにバージョン表示を追加する
実際のコード変更を題材に、アプリケーション更新の全フローを実践します。TaskBoard APIに/api/versionエンドポイントを追加し、アプリケーションバージョンを返す機能を実装します。
現在のTaskBoard APIのソースコードは~/k8s-production/taskboard-api/にあります。バージョンエンドポイントを追加するために、新しいJAX-RSリソースクラスを作成します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard-api/src/main/java/com/taskboard/VersionResource.java
package com.taskboard;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.json.Json;
import jakarta.json.JsonObject;
@Path("/version")
public class VersionResource {
// アプリケーションバージョン(更新時にここを変更する)
private static final String APP_VERSION = "2.1.0";
@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getVersion() {
return Json.createObjectBuilder()
.add("application", "TaskBoard API")
.add("version", APP_VERSION)
.add("runtime", "Payara Micro 7.2026.1")
.build();
}
}
EOF
変更内容はシンプルです。/api/versionにGETリクエストを送ると、アプリケーション名、バージョン、ランタイム情報をJSON形式で返します。バージョン番号は第5回でtaskboard-api:2.0.0としてビルドしたため、今回の更新で2.1.0とします。
8.3.2 Dockerイメージを再ビルドしてkindに投入する
コード変更が完了したら、Dockerイメージを再ビルドします。タグには新しいバージョン番号2.1.0を付与します。
[Execution User: developer]
cd ~/k8s-production/taskboard-api
docker build -t taskboard-api:2.1.0 .
multi-stage buildにより、Mavenビルドステージでソースコードがコンパイルされ、実行ステージでpayara/micro:7.2026.1ベースのイメージが作成されます。ビルドの最終行にnaming to docker.io/library/taskboard-api:2.1.0と表示されれば成功です。
ビルドしたイメージをkindクラスタに投入します。
[Execution User: developer]
kind load docker-image taskboard-api:2.1.0 --name k8s-applied
Image: "taskboard-api:2.1.0" with ID "sha256:b2c3d4e5..." not yet present on node "k8s-applied-worker", loading...
Image: "taskboard-api:2.1.0" with ID "sha256:b2c3d4e5..." not yet present on node "k8s-applied-worker2", loading...
Image: "taskboard-api:2.1.0" with ID "sha256:b2c3d4e5..." not yet present on node "k8s-applied-worker3", loading...
Image: "taskboard-api:2.1.0" with ID "sha256:b2c3d4e5..." not yet present on node "k8s-applied-control-plane", loading...
全Nodeにイメージが配布されました。次に、Deploymentのイメージタグを更新するマニフェストを準備します。
[Execution User: developer]
# 現在のイメージタグを確認
kubectl get deployment taskboard-api -n app -o jsonpath='{.spec.template.spec.containers[0].image}'
taskboard-api:2.0.0
マニフェストファイルのイメージタグを2.1.0に更新します。
[Execution User: developer]
sed -i 's/taskboard-api:2.0.0/taskboard-api:2.1.0/' ~/k8s-production/manifests/taskboard-api-deployment.yaml
8.3.3 安全なデプロイフロー — dry-run → diff → apply → 確認
第5回(アプリケーション構築)で導入した安全なデプロイフローを、今度は「更新」の文脈で実践します。第7回の運用設計書で定めたデプロイ手順に沿って進めます。
Step 1: dry-run(サーバーサイド検証)
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/taskboard-api-deployment.yaml --dry-run=server
deployment.apps/taskboard-api configured (server dry run)
configuredと表示されました。新規作成ではなく既存リソースの更新であることが分かります。APIサーバーのバリデーションをパスしているので、マニフェストの構文に問題はありません。
Step 2: diff(差分確認)
[Execution User: developer]
kubectl diff -f ~/k8s-production/manifests/taskboard-api-deployment.yaml
diff -u -N /tmp/LIVE-xxxx /tmp/MERGED-xxxx
--- /tmp/LIVE-xxxx
+++ /tmp/MERGED-xxxx
@@ -32,7 +32,7 @@
containers:
- name: taskboard-api
- image: taskboard-api:2.0.0
+ image: taskboard-api:2.1.0
ports:
変更点はイメージタグの2.0.0→2.1.0のみです。意図した差分だけが含まれていることを確認します。予期しない差分が混入していないか——これがdiffの目的です。
Step 3: apply(適用)
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/taskboard-api-deployment.yaml
deployment.apps/taskboard-api configured
Step 4: ロールアウト完了の確認
[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...
deployment "taskboard-api" successfully rolled out
第7回で設計したmaxSurge: 1, maxUnavailable: 0の設定により、新しいPodが先に起動してReadyになってから、古いPodが1つずつ停止していきます。Payara Microの起動に15〜20秒かかるため、ローリングアップデート全体には1分程度かかります。
Step 5: 新バージョンの動作確認
[Execution User: developer]
# Gateway API経由で新しいエンドポイントにアクセス
GATEWAY_IP=$(kubectl get svc -n envoy-gateway-system envoy-default-taskboard-gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "localhost")
GATEWAY_PORT=$(kubectl get svc -n envoy-gateway-system envoy-default-taskboard-gateway -o jsonpath='{.spec.ports[0].nodePort}')
curl -s http://${GATEWAY_IP}:${GATEWAY_PORT}/api/version | python3 -m json.tool
{
"application": "TaskBoard API",
"version": "2.1.0",
"runtime": "Payara Micro 7.2026.1"
}
バージョン2.1.0が返されました。新しいエンドポイントが正常に動作しています。既存のAPIも確認しておきます。
[Execution User: developer]
# 既存のタスクAPIが引き続き動作していることを確認
curl -s http://${GATEWAY_IP}:${GATEWAY_PORT}/api/tasks | python3 -m json.tool
既存のタスク一覧が返されれば、デグレは発生していません。これで安全なデプロイフローの全5ステップが完了しました。
8.3.4 ロールバック判断と実行
デプロイ後に問題が発覚した場合のロールバック手順を確認します。第7回の運用設計書では「デプロイ後5分以内に異常を検知した場合は即座にロールバック」と定めました。
まず、ロールアウト履歴を確認します。
[Execution User: developer]
kubectl rollout history deployment/taskboard-api -n app
deployment.apps/taskboard-api
REVISION CHANGE-CAUSE
1 <none>
2 <none>
REVISION 1が旧バージョン(2.0.0)、REVISION 2が今回デプロイしたバージョン(2.1.0)です。ロールバックを実行します。
[Execution User: developer]
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
[Execution User: developer]
# ロールバック後のイメージタグを確認
kubectl get deployment taskboard-api -n app -o jsonpath='{.spec.template.spec.containers[0].image}'
taskboard-api:2.0.0
イメージが2.0.0に戻りました。ロールバックが完了です。
確認が取れたので、再度2.1.0にデプロイし直します。今回はロールバックの手順確認が目的だったためです。
[Execution User: developer]
kubectl apply -f ~/k8s-production/manifests/taskboard-api-deployment.yaml
kubectl rollout status deployment/taskboard-api -n app
以上がアプリケーション更新の実務フローです。変更管理手順書には以下の手順を記載します。
変更管理手順書 — 1. アプリケーション更新手順
1.1 コード変更 → ビルド → イメージ作成
① ソースコードを変更する
②docker build -t <image>:<new-tag> .でイメージをビルドする
③kind load docker-image <image>:<new-tag> --name k8s-appliedでクラスタに投入する
④ マニフェストのイメージタグを新しいタグに更新する1.2 デプロイ手順
①kubectl apply --dry-run=serverで事前検証
②kubectl diffで差分確認(意図した変更のみか)
③kubectl applyで適用
④kubectl rollout statusでロールアウト完了を確認
⑤ 動作確認(新機能 + 既存機能のデグレ確認)1.3 ロールバック手順
①kubectl rollout undo deployment/<name> -n <namespace>
②kubectl rollout statusでロールバック完了を確認
③ ロールバック後の動作確認
④ 原因調査 → 修正 → 再デプロイ
8.4 ConfigMap / Secret変更の反映
8.4.1 ConfigMap変更の反映方法と注意点
ConfigMapの変更がPodにどう反映されるかは、ConfigMapの使い方によって異なります。この違いを理解していないと「変更したのに反映されない」というトラブルに遭遇します。
| 使い方 | 反映タイミング | 備考 |
|---|---|---|
| ボリュームマウント | 自動更新(ただし最大60〜90秒の遅延) | kubeletのsync周期に依存。subPathを使っている場合は自動更新されない |
| 環境変数として参照 | Podの再起動が必要(自動では反映されない) | 環境変数はPod起動時に固定される |
TaskBoardのNginxでは、ConfigMapをボリュームマウント + subPathで使用しています。subPathを使っている場合、ConfigMapの変更は自動では反映されません。
実際に確認してみましょう。Nginx ConfigMapにカスタムエラーページの設定を追加します。
[Execution User: developer]
# 現在のConfigMapの内容を確認
kubectl get configmap nginx-config -n app -o yaml | head -20
ConfigMapを更新します。nginx.confにカスタムエラーページのディレクティブを追加する例を示します。
[Execution User: developer]
# ConfigMapを直接編集(server_tokensをoffに変更する例)
kubectl get configmap nginx-config -n app -o yaml > /tmp/nginx-configmap-backup.yaml
# server_tokens off; を追加(すでに含まれている場合は別の軽微な変更で代替)
kubectl edit configmap nginx-config -n app
編集後、Podに反映されているか確認します。
[Execution User: developer]
# Nginx Podの中でマウントされた設定ファイルを確認
NGINX_POD=$(kubectl get pods -n app -l component=frontend -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n app $NGINX_POD -- cat /etc/nginx/nginx.conf | head -5
subPathでマウントしているため、ConfigMapを変更しても自動更新は行われません。これは重要な注意点です。ボリュームマウント(subPathなし)の場合は最大60〜90秒で自動更新されますが、subPath付きの場合はPodの再起動が必要です。
8.4.2 Secret変更の反映方法
Secretの反映方法はConfigMapと同じルールに従います。TaskBoardのMySQL Secretは環境変数としてStatefulSetから参照されているため、Podの再起動なしには反映されません。
Secretの変更が必要になるシナリオとしては、データベースパスワードのローテーションがあります。この場合、MySQLの認証情報とアプリケーション側の接続情報の両方を同時に更新する必要があり、手順の順序が重要です。
| Step | 操作 | 影響 |
|---|---|---|
| 1 | Secret(mysql-secret)を更新 | 既存Podには影響なし(環境変数は起動時に固定) |
| 2 | MySQL側のパスワードを変更 | 既存の接続は維持される(セッション中) |
| 3 | TaskBoard API Podを再起動 | 新しいSecretで接続し直す |
| 4 | 動作確認 | API → MySQL接続が正常か確認 |
パスワードローテーションは本番運用では重要ですが、kindの学習環境では手順の把握に留めます。
8.4.3 kubectl rollout restartによる確実な反映
ConfigMapやSecretの変更を確実にPodに反映させるには、kubectl rollout restartを使います。
[Execution User: developer]
kubectl rollout restart deployment/nginx -n app
deployment.apps/nginx restarted
[Execution User: developer]
kubectl rollout status deployment/nginx -n app
deployment "nginx" successfully rolled out
rollout restartはDeploymentのPod templateにアノテーション(kubectl.kubernetes.io/restartedAt)を追加することで、ローリングアップデートをトリガーします。イメージの変更はありませんが、Podが再作成されるため、ConfigMapやSecretの最新値が反映されます。
反映結果を確認します。
[Execution User: developer]
# 新しいPodで設定が反映されていることを確認
NGINX_POD=$(kubectl get pods -n app -l component=frontend -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n app $NGINX_POD -- cat /etc/nginx/nginx.conf | head -5
更新した内容が反映されていることを確認してください。
変更管理手順書 — 2. 設定変更手順
2.1 ConfigMap変更と反映
① 変更前のConfigMapをバックアップ(kubectl get configmap <name> -n <ns> -o yaml > backup.yaml)
② ConfigMapを更新(kubectl editorkubectl apply -f)
③ 反映方法を選択:ボリュームマウント(subPathなし)→ 自動反映を待つ / それ以外 →rollout restart2.2 Secret変更と反映
① 対象Secretを特定し、影響するPodを洗い出す
② Secretを更新する
③ 関連するDeployment/StatefulSetをrollout restartする2.3 反映確認手順
① Pod内で設定値が更新されていることをkubectl execで確認
② アプリケーションの動作確認
8.5 TaskBoard全体のHelm化
8.5.1 応用編第9回の振り返り — フロントのみのHelm化
応用編第9回で、TaskBoardのフロントエンド(Nginx)をHelm化しました。Chart.yaml、values.yaml、_helpers.tpl、Deploymentテンプレート、Serviceテンプレートの5ファイルからなるシンプルなチャートでした。そこでは「Helmの使い方を知る」ことに集中し、1コンポーネントだけを対象としました。
本回では、TaskBoardの全コンポーネントを1つのHelmチャートにまとめます。応用編で学んだvalues.yamlの設計原則——「環境ごとに変わる値はvalues.yamlへ、変えるとシステムが壊れる値はテンプレートにハードコード」——を全コンポーネントに適用します。
8.5.2 チャート構造の設計
TaskBoard全体のHelmチャートのディレクトリ構造を設計します。
[Execution User: developer]
mkdir -p ~/k8s-production/taskboard/templates
taskboard/
├── Chart.yaml # チャートのメタデータ
├── values.yaml # デフォルト値
├── values-dev.yaml # 開発環境オーバーライド
├── values-prod.yaml # 本番環境オーバーライド
├── templates/
│ ├── _helpers.tpl # 共通ヘルパー(ラベル、名前生成)
│ ├── nginx-deployment.yaml
│ ├── nginx-service.yaml
│ ├── nginx-hpa.yaml
│ ├── nginx-pdb.yaml
│ ├── taskboard-api-deployment.yaml
│ ├── taskboard-api-service.yaml
│ ├── taskboard-api-hpa.yaml
│ ├── taskboard-api-pdb.yaml
│ ├── mysql-statefulset.yaml
│ ├── mysql-headless-service.yaml
│ ├── mysql-secret.yaml
│ ├── nginx-configmap.yaml
│ ├── db-init-job.yaml
│ ├── db-backup-cronjob.yaml
│ ├── log-collector-daemonset.yaml
│ └── NOTES.txt # helm install後の案内メッセージ
└── .helmignore
応用編のフロントエンドチャート(5ファイル)から、全コンポーネント対応の17テンプレート + 4設定ファイルの構成に拡張します。
ここで設計判断が必要です。Gateway API(HTTPRoute)とNetworkPolicyをこのチャートに含めるか、別管理にするか。
| 方針 | メリット | デメリット |
|---|---|---|
| チャートに含める | 1チャートで全体管理。helm uninstallで完全クリーンアップ | アプリチャートにNWリソースが混在 |
| 別管理にする | NWリソースはインフラ層の責務として分離。チーム間の責任分界が明確 | 管理対象が増える |
本シリーズでは学習目的の観点から、1つのチャートに含めず、Gateway APIとNetworkPolicyはkubectl applyでの別管理を維持します。本番環境でも、ネットワークリソースはインフラチームの管轄として分離することが多いため、この判断は実務にも沿っています。RBACやResourceQuota/LimitRangeも同様に、クラスタ基盤の管理として別管理とします。
8.5.3 values.yamlの設計 — 何を切り出すか
values.yamlに切り出すパラメータの選定は、Helmチャート設計の核心です。応用編第9回で学んだ原則を全コンポーネントに適用します。
| 判断 | 項目 | 理由 |
|---|---|---|
| values.yamlに切り出す | replicas数、resources(requests / limits) | dev/prodで必ず差分が出る |
| values.yamlに切り出す | イメージタグ(image.tag) | 更新のたびに変わる |
| values.yamlに切り出す | HPA閾値(minReplicas, maxReplicas, targetCPU) | 環境の負荷特性に応じて変わる |
| values.yamlに切り出す | CronJobのスケジュール | dev(短間隔テスト)とprod(日次)で異なる |
| values.yamlに切り出す | MySQL認証情報(Secret値) | 環境ごとに異なるべき |
| テンプレートにハードコード | ラベル体系(app, component等) | Service/NetworkPolicyのセレクタと連動 |
| テンプレートにハードコード | ポート番号(8080, 3306等) | アプリの設定と密結合 |
| テンプレートにハードコード | Probeのエンドポイントパス | MicroProfile Healthの仕様で固定 |
| テンプレートにハードコード | SecurityContext設定 | セキュリティ要件は環境で変えるべきでない |
まずChart.yamlを作成します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/Chart.yaml
apiVersion: v2
name: taskboard
description: TaskBoard application - Nginx frontend, Payara Micro API, MySQL database
type: application
version: 1.0.0 # チャート自体のバージョン
appVersion: "2.1.0" # TaskBoard APIのバージョン
EOF
次にvalues.yamlを作成します。これが全コンポーネントのデフォルトパラメータです。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/values.yaml
# =============================================================
# TaskBoard Helm Chart - デフォルト値(prod環境相当)
# =============================================================
# --- Nginx(フロントエンド) ---
nginx:
replicaCount: 2
image:
repository: nginx
tag: "1.27"
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
hpa:
enabled: true
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
pdb:
enabled: true
minAvailable: 1
# --- TaskBoard API ---
api:
replicaCount: 2
image:
repository: taskboard-api
tag: "2.1.0"
resources:
requests:
cpu: "200m"
memory: "384Mi"
limits:
cpu: "500m"
memory: "512Mi"
hpa:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
pdb:
enabled: true
minAvailable: 1
# --- MySQL ---
mysql:
image:
repository: mysql
tag: "8.0"
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
auth:
rootPassword: "TaskB0ard-Root-2026"
database: "taskboard"
user: "taskboard"
password: "TaskB0ard-App-2026"
storage:
size: "1Gi"
# --- DB初期化Job ---
dbInit:
image:
repository: mysql
tag: "8.0"
# --- DBバックアップCronJob ---
dbBackup:
schedule: "0 2 * * *" # 毎日 AM 2:00(本番)
image:
repository: mysql
tag: "8.0"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
# --- ログ収集DaemonSet ---
logCollector:
image:
repository: busybox
tag: "1.36"
EOF
values.yamlの設計ポイントを整理します。コンポーネントごとにネストした構造(nginx.、api.、mysql.)にすることで、テンプレート内での参照が明快になります。HPAとPDBはenabledフラグで有効/無効を切り替えられるようにしました。dev環境ではHPAを無効にする運用が想定されるためです。
続いて、共通ヘルパーテンプレートを作成します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/templates/_helpers.tpl
{{/*
チャート名を返す
*/}}
{{- define "taskboard.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Nginx用の共通ラベルを返す
*/}}
{{- define "taskboard.nginx.labels" -}}
app: taskboard
component: frontend
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}
{{/*
Nginx用のセレクタラベルを返す
*/}}
{{- define "taskboard.nginx.selectorLabels" -}}
app: taskboard
component: frontend
{{- end }}
{{/*
TaskBoard API用の共通ラベルを返す
*/}}
{{- define "taskboard.api.labels" -}}
app: taskboard
component: api
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}
{{/*
TaskBoard API用のセレクタラベルを返す
*/}}
{{- define "taskboard.api.selectorLabels" -}}
app: taskboard
component: api
{{- end }}
{{/*
MySQL用の共通ラベルを返す
*/}}
{{- define "taskboard.mysql.labels" -}}
app: taskboard
component: db
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}
{{/*
MySQL用のセレクタラベルを返す
*/}}
{{- define "taskboard.mysql.selectorLabels" -}}
app: taskboard
component: db
{{- end }}
EOF
応用編ではフロントエンド用のラベルテンプレートだけでしたが、全コンポーネント用にNginx、API、MySQLの3セットを定義しています。ラベル体系(app: taskboard、component: frontend/api/db)は第1回から一貫して使ってきたものをそのまま維持します。
8.5.4 テンプレートの作成
テンプレートは17ファイルありますが、全文を掲載すると記事が膨大になります。代表的なテンプレート(TaskBoard API Deployment、MySQL StatefulSet)を全文掲載し、残りは構造と要点を説明します。
TaskBoard API Deployment テンプレート
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/templates/taskboard-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskboard-api
namespace: app
labels:
{{- include "taskboard.api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.api.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
{{- include "taskboard.api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "taskboard.api.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: taskboard-api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
ports:
- containerPort: 8080
resources:
requests:
cpu: "{{ .Values.api.resources.requests.cpu }}"
memory: "{{ .Values.api.resources.requests.memory }}"
limits:
cpu: "{{ .Values.api.resources.limits.cpu }}"
memory: "{{ .Values.api.resources.limits.memory }}"
startupProbe:
httpGet:
path: /health/started
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
failureThreshold: 10
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 3
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
env:
- name: MYSQL_HOST
value: "mysql-headless.db.svc.cluster.local"
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_DATABASE
value: "taskboard"
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: mysql-secret-app
key: mysql-user
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret-app
key: mysql-password
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
EOF
values.yamlから注入される部分({{ .Values.api.xxx }})とハードコードされた部分を比較してください。replicas、image、resourcesはvalues.yamlから取得しますが、Probeのエンドポイントパス(/health/started等)、ポート番号(8080)、SecurityContext、環境変数のキー名はテンプレートに固定しています。strategyのmaxSurge: 1, maxUnavailable: 0も第7回の運用設計書で決定した値であり、環境差分ではないため固定です。
MySQL StatefulSet テンプレート
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/templates/mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: db
labels:
{{- include "taskboard.mysql.labels" . | nindent 4 }}
spec:
serviceName: mysql-headless
replicas: 1
selector:
matchLabels:
{{- include "taskboard.mysql.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "taskboard.mysql.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: mysql
image: "{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}"
ports:
- containerPort: 3306
resources:
requests:
cpu: "{{ .Values.mysql.resources.requests.cpu }}"
memory: "{{ .Values.mysql.resources.requests.memory }}"
limits:
cpu: "{{ .Values.mysql.resources.limits.cpu }}"
memory: "{{ .Values.mysql.resources.limits.memory }}"
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: mysql-root-password
- name: MYSQL_DATABASE
value: "taskboard"
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: mysql-secret
key: mysql-user
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: mysql-password
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
periodSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
periodSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.mysql.storage.size }}
EOF
StatefulSetはDeploymentと異なりvolumeClaimTemplatesを持ちます。ストレージサイズはvalues.yamlで管理し、dev環境では小さく、prod環境では大きく設定できるようにしています。replicas: 1は基本設計(第2回)で決定した値であり、シングルレプリカ構成のため固定しています。
残りのテンプレートの構造
残りのテンプレートは、上記2つと同じパターンで作成します。各テンプレートの要点を示します。
| テンプレート | values.yamlから取得する値 | ハードコードする値 |
|---|---|---|
| nginx-deployment.yaml | replicaCount, image, resources | port: 8080, SecurityContext, volumeMounts |
| nginx-service.yaml | (なし — 全て固定) | type: ClusterIP, port: 80, targetPort: 8080 |
| nginx-hpa.yaml | minReplicas, maxReplicas, targetCPU | 対象Deployment名 |
| nginx-pdb.yaml | minAvailable | セレクタラベル |
| taskboard-api-service.yaml | (なし) | type: ClusterIP, port: 80, targetPort: 8080 |
| taskboard-api-hpa.yaml | minReplicas, maxReplicas, targetCPU | 対象Deployment名 |
| taskboard-api-pdb.yaml | minAvailable | セレクタラベル |
| mysql-headless-service.yaml | (なし) | clusterIP: None, port: 3306 |
| mysql-secret.yaml | auth.*(base64エンコード) | Secret名 |
| nginx-configmap.yaml | (なし — nginx.confは固定) | nginx.confの内容 |
| db-init-job.yaml | image | 初期化SQL、Secret参照 |
| db-backup-cronjob.yaml | schedule, image | バックアップコマンド |
| log-collector-daemonset.yaml | image | volumeMounts(/var/log) |
各テンプレートは~/k8s-production/manifests/にある既存のマニフェストをベースに、可変部分を{{ .Values.xxx }}に置き換えるだけです。全テンプレートの作成コマンドは省略しますが、上記のTaskBoard API DeploymentとMySQL StatefulSetのパターンに従って作成してください。
HPAテンプレートには条件分岐を入れます。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/templates/nginx-hpa.yaml
{{- if .Values.nginx.hpa.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
namespace: app
labels:
{{- include "taskboard.nginx.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
minReplicas: {{ .Values.nginx.hpa.minReplicas }}
maxReplicas: {{ .Values.nginx.hpa.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.nginx.hpa.targetCPUUtilizationPercentage }}
{{- end }}
EOF
{{- if .Values.nginx.hpa.enabled }}で囲むことで、values.yamlのhpa.enabled: falseでHPAを無効にできます。PDBも同様のパターンです。dev環境では1レプリカで運用するため、HPAやPDBは不要になります。
最後にNOTES.txtを作成します。helm install後に表示される案内メッセージです。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/templates/NOTES.txt
=== TaskBoard deployed ===
Release: {{ .Release.Name }}
Namespace: app (frontend, API), db (MySQL)
TaskBoard API version: {{ .Values.api.image.tag }}
Nginx replicas: {{ .Values.nginx.replicaCount }}
API replicas: {{ .Values.api.replicaCount }}
To verify:
kubectl get pods -n app
kubectl get pods -n db
To access via Gateway API:
GATEWAY_PORT=$(kubectl get svc -n envoy-gateway-system envoy-default-taskboard-gateway -o jsonpath='{.spec.ports[0].nodePort}')
curl http://localhost:${GATEWAY_PORT}/api/version
EOF
8.5.5 helm installでTaskBoardをデプロイする
既存のkubectlデプロイ済みリソースをHelm管理に移行します。学習環境のため、一度既存リソースを削除してからhelm installする方法を取ります。
[Execution User: developer]
# 既存のアプリケーションリソースを削除(基盤リソースは残す)
# app namespaceのワークロード
kubectl delete deployment nginx taskboard-api -n app
kubectl delete service nginx taskboard-api -n app
kubectl delete hpa nginx-hpa taskboard-api-hpa -n app
kubectl delete pdb nginx-pdb taskboard-api-pdb -n app
kubectl delete configmap nginx-config -n app
# db namespaceのワークロード
kubectl delete statefulset mysql -n db
kubectl delete service mysql-headless -n db
kubectl delete secret mysql-secret mysql-secret-app -n db
kubectl delete job db-init -n db
kubectl delete cronjob db-backup -n db
# monitoring namespaceのワークロード
kubectl delete daemonset log-collector -n monitoring
Namespace、ResourceQuota、LimitRange、RBAC、Gateway API、NetworkPolicyはHelm管理外として残します。PVCもStatefulSetの削除で自動削除されないため、データは保持されます。
本番環境での移行について
本番環境では、稼働中のリソースを削除してからHelm化する方法はサービス断を伴うため推奨されません。
kubectl annotateとkubectl labelで既存リソースにHelm管理用のメタデータを付与し、helm install時にリソースを引き継ぐ方法があります。また、helm install --adoptのような手法も検討されています。移行戦略は環境の要件に応じて選択してください。
チャートをインストールします。
[Execution User: developer]
# テンプレートのレンダリングを事前確認
helm template taskboard ~/k8s-production/taskboard | head -50
テンプレートが正しくレンダリングされることを確認したら、インストールします。Helmのリリース名はtaskboardとします。Namespaceは各テンプレート内で指定しているため、-nフラグは不要です(ただしHelm自体のリリース情報を格納するNamespaceとして-n appを指定します)。
[Execution User: developer]
helm install taskboard ~/k8s-production/taskboard -n app
NAME: taskboard
LAST DEPLOYED: ...
NAMESPACE: app
STATUS: deployed
REVISION: 1
NOTES:
=== TaskBoard deployed ===
Release: taskboard
Namespace: app (frontend, API), db (MySQL)
TaskBoard API version: 2.1.0
Nginx replicas: 2
API replicas: 2
To verify:
kubectl get pods -n app
kubectl get pods -n db
...
NOTES.txtの内容が表示されました。TaskBoardが正常稼働しているか確認します。
[Execution User: developer]
kubectl get pods -n app
kubectl get pods -n db
kubectl get pods -n monitoring
全PodがRunning(JobはCompleted)であることを確認します。Gateway API経由でのアクセスも確認しておきましょう。
[Execution User: developer]
GATEWAY_PORT=$(kubectl get svc -n envoy-gateway-system envoy-default-taskboard-gateway -o jsonpath='{.spec.ports[0].nodePort}')
curl -s http://localhost:${GATEWAY_PORT}/api/version | python3 -m json.tool
8.5.6 helm upgradeで更新を実行する
Helm管理下でのアプリケーション更新を試します。values.yamlのイメージタグを変更し、helm upgradeで適用します。
[Execution User: developer]
# イメージタグを変更してアップグレード
helm upgrade taskboard ~/k8s-production/taskboard \
--set api.image.tag="2.1.0" \
-n app
--setフラグでvalues.yamlの値を一時的に上書きできます。新しいバージョンのイメージがある場合はタグを変更してupgradeすれば、ローリングアップデートが実行されます。
[Execution User: developer]
helm list -n app
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
taskboard app 2 2026-... deployed taskboard-1.0.0 2.1.0
REVISIONが2に増えています。helm upgradeのたびにリビジョンが増加し、リリース履歴が記録されます。
8.6 values.yamlによる環境差分管理
8.6.1 dev環境とprod環境のvalues設計
環境ごとにvalues.yamlを分けます。デフォルトのvalues.yamlをprod相当とし、dev環境用のオーバーライドファイルを作成します。
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/values-dev.yaml
# =============================================================
# TaskBoard - 開発環境オーバーライド
# リソース節約のため、レプリカ1、リソース小、HPA/PDB無効
# =============================================================
nginx:
replicaCount: 1
resources:
requests:
cpu: "25m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
hpa:
enabled: false
pdb:
enabled: false
api:
replicaCount: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "300m"
memory: "384Mi"
hpa:
enabled: false
pdb:
enabled: false
dbBackup:
schedule: "*/5 * * * *" # 開発環境では5分間隔(動作確認用)
EOF
[Execution User: developer]
cat <<'EOF' > ~/k8s-production/taskboard/values-prod.yaml
# =============================================================
# TaskBoard - 本番環境オーバーライド
# values.yaml のデフォルトがprod相当のため、差分のみ記載
# =============================================================
# デフォルトのvalues.yamlがprod相当のため、
# 明示的に上書きする項目がある場合のみ記載する。
# 例:本番環境で特別にリソースを増やす場合
#
# api:
# resources:
# limits:
# cpu: "1000m"
# memory: "1Gi"
EOF
dev環境とprod環境の差分を一覧で確認しましょう。
| 項目 | dev(values-dev.yaml) | prod(values.yaml デフォルト) |
|---|---|---|
| Nginx replicas | 1 | 2 |
| Nginx CPU requests/limits | 25m / 100m | 50m / 200m |
| Nginx Memory requests/limits | 32Mi / 64Mi | 64Mi / 128Mi |
| Nginx HPA | 無効 | 有効(min:2, max:6) |
| Nginx PDB | 無効 | 有効(minAvailable:1) |
| API replicas | 1 | 2 |
| API CPU requests/limits | 100m / 300m | 200m / 500m |
| API Memory requests/limits | 256Mi / 384Mi | 384Mi / 512Mi |
| API HPA | 無効 | 有効(min:2, max:4) |
| API PDB | 無効 | 有効(minAvailable:1) |
| DB Backup Schedule | 5分間隔 | 毎日AM2:00 |
8.6.2 値を切り替えてデプロイする
dev環境の値に切り替えてみましょう。
[Execution User: developer]
helm upgrade taskboard ~/k8s-production/taskboard \
-f ~/k8s-production/taskboard/values-dev.yaml \
-n app
Release "taskboard" has been upgraded. Happy Helming!
[Execution User: developer]
# replicas数が変わったことを確認
kubectl get deployment -n app
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 5m
taskboard-api 1/1 1 1 5m
replicas が2から1に変更されました。HPAも無効になっていることを確認します。
[Execution User: developer]
kubectl get hpa -n app
No resources found in app namespace.
prod環境に戻します。
[Execution User: developer]
helm upgrade taskboard ~/k8s-production/taskboard -n app
-fフラグを付けなければ、デフォルトのvalues.yaml(prod相当)が使われます。replicas: 2、HPA有効の状態に復元されます。
8.6.3 helm historyでリリース履歴を管理する
[Execution User: developer]
helm history taskboard -n app
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 2026-... superseded taskboard-1.0.0 2.1.0 Install complete
2 2026-... superseded taskboard-1.0.0 2.1.0 Upgrade complete
3 2026-... superseded taskboard-1.0.0 2.1.0 Upgrade complete
4 2026-... deployed taskboard-1.0.0 2.1.0 Upgrade complete
4つのリビジョンが記録されています。REVISION 1がinitial install、2がhelm upgrade、3がdev環境への切り替え、4がprod環境への復元です。
Helmのrollbackで特定のリビジョンに戻すことができます。
[Execution User: developer]
# REVISION 3(dev環境)に戻す
helm rollback taskboard 3 -n app
Rollback was a success! Happy Helming!
[Execution User: developer]
helm history taskboard -n app
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 2026-... superseded taskboard-1.0.0 2.1.0 Install complete
2 2026-... superseded taskboard-1.0.0 2.1.0 Upgrade complete
3 2026-... superseded taskboard-1.0.0 2.1.0 Upgrade complete
4 2026-... superseded taskboard-1.0.0 2.1.0 Upgrade complete
5 2026-... deployed taskboard-1.0.0 2.1.0 Rollback to 3
REVISION 5として「Rollback to 3」が記録されました。ロールバック自体も履歴として残ります。誰がいつどのリビジョンに戻したか——変更管理に必要な情報がhelm historyで一元管理されます。
prod環境に戻しておきます。
[Execution User: developer]
helm upgrade taskboard ~/k8s-production/taskboard -n app
変更管理手順書 — 3. Helmによる変更管理
3.1 helm upgrade手順
① 変更内容をvalues.yamlまたは--setフラグで準備
②helm templateでレンダリング結果を事前確認
③helm upgrade <release> <chart> [-f values-xxx.yaml] -n <ns>で適用
④kubectl rollout statusで各Deploymentのロールアウト完了を確認
⑤ 動作確認3.2 helm rollback手順
①helm history <release> -n <ns>で対象リビジョンを確認
②helm rollback <release> <revision> -n <ns>でロールバック
③ ロールバック完了後の動作確認3.3 helm historyによる履歴管理
変更のたびにhelm historyを確認し、リビジョン番号と変更内容を記録する
)、メモリ枯渇(OOMKilled)、Node障害、ネットワーク障害、ストレージ障害、設定ミス——6つの障害パターンを注入し、切り分け・復旧・再発防止のプロセスを実践します。
8.7 ノードメンテナンス — cordon / drain / uncordon
8.7.1 ノードメンテナンスが必要になるシナリオ
K8sクラスタのNodeは物理サーバーまたは仮想マシンであり、定期的なメンテナンスが必要です。OSのカーネルアップデート、セキュリティパッチの適用、Docker(コンテナランタイム)のアップグレード、ハードウェア交換——これらの作業中、Node上で稼働するPodは一時的に別のNodeに退避させなければなりません。
VMの世界では、ESXiホストにパッチを適用するとき、vMotionでVMを別ホストに移動し、ホストをメンテナンスモードにする手順でした。DRS(Distributed Resource Scheduler)が有効であれば、VMの移動は自動で行われました。K8sではcordon→drain→作業→uncordonという3ステップでこれを実現します。
| 操作 | VMの世界 | K8sの世界 |
|---|---|---|
| 新規配置の停止 | ホストをメンテナンスモードに移行 | kubectl cordon <node> |
| 既存ワークロードの退避 | vMotionでVMをライブマイグレーション | kubectl drain <node>(Podを退避・再配置) |
| メンテナンス作業 | ESXiパッチ適用 → リブート | OSパッチ適用、Docker更新等 |
| 復帰 | メンテナンスモードを解除 | kubectl uncordon <node> |
| 可用性の保証 | DRSのアフィニティ/アンチアフィニティ | PDB(PodDisruptionBudget) |
大きな違いは、vMotionが「ライブマイグレーション」(VM自体を止めずに移動)であるのに対し、K8sのdrainは「Podを停止して別Nodeに再作成」する点です。Podにはメモリ上の状態が保持されないため(ステートレスが前提)、新しいNodeで新しいPodが起動する形になります。PDBが設定されていれば、サービスの可用性を維持しながら段階的に退避が行われます。
8.7.2 cordonでNodeへの新規配置を停止する
まず現在のNode一覧を確認します。
[Execution User: developer]
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-applied-control-plane Ready control-plane ... v1.32.x
k8s-applied-worker Ready <none> ... v1.32.x
k8s-applied-worker2 Ready <none> ... v1.32.x
k8s-applied-worker3 Ready <none> ... v1.32.x
Worker Nodeの1つを選択してcordonします。Podが多く配置されているNodeを選ぶと、drainの効果が分かりやすくなります。
[Execution User: developer]
# cordon前のPod配置状況を確認
kubectl get pods -A -o wide | grep -v "kube-system\|envoy-gateway\|calico"
メンテナンス対象のNodeをk8s-applied-workerとしてcordonします。
[Execution User: developer]
kubectl cordon k8s-applied-worker
node/k8s-applied-worker cordoned
[Execution User: developer]
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-applied-control-plane Ready control-plane ... v1.32.x
k8s-applied-worker Ready,SchedulingDisabled <none> ... v1.32.x
k8s-applied-worker2 Ready <none> ... v1.32.x
k8s-applied-worker3 Ready <none> ... v1.32.x
SchedulingDisabledが表示されました。cordonされたNodeには新しいPodがスケジュールされなくなりますが、既存のPodはそのまま稼働を続けます。cordonは「新規受付停止」であり、「既存の追い出し」ではありません。
8.7.3 drainでPodを安全に退避させる — PDBの効果を確認
ここが本回のハイライトです。kubectl drainでNode上のPodを退避させます。そして、第2回(基本設計)で設計し、第5回(アプリケーション構築)で適用したPDBが、ここで効果を発揮します。
drain実行前に、PDBの設定を確認しておきましょう。
[Execution User: developer]
kubectl get pdb -n app
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
nginx-pdb 1 N/A 1 ...
taskboard-api-pdb 1 N/A 1 ...
minAvailable: 1が設定されています。drainでPodを退避させる際、最低1つのPodが常にReadyであることが保証されます。2つのPodが同じNodeに配置されていても、1つずつ段階的に退避し、サービスの継続性が維持されます。
drainを実行します。
[Execution User: developer]
kubectl drain k8s-applied-worker --ignore-daemonsets --delete-emptydir-data
node/k8s-applied-worker already cordoned
WARNING: ignoring DaemonSet-managed Pods: monitoring/log-collector-xxxxx, kube-system/...
evicting pod app/nginx-xxxxx
evicting pod app/taskboard-api-xxxxx
pod/nginx-xxxxx evicted
pod/taskboard-api-xxxxx evicted
node/k8s-applied-worker drained
drainコマンドのオプションを整理します。
| オプション | 意味 |
|---|---|
--ignore-daemonsets |
DaemonSet管理のPodを無視する。DaemonSetはNode固有のPodであり、退避させる必要がない |
--delete-emptydir-data |
emptyDirボリュームを持つPodの退避を許可する。emptyDirのデータは退避時に消失する |
--ignore-daemonsetsを指定しないと、DaemonSet管理のPod(ログ収集DaemonSet等)が退避できずにdrainが失敗します。DaemonSetのPodはNodeが存在する限り再作成されるため、退避させる必要はありません。
drain後のPod配置状況を確認します。
[Execution User: developer]
kubectl get pods -n app -o wide
退避させたPodがk8s-applied-worker2やk8s-applied-worker3に再配置されていることを確認してください。k8s-applied-workerにはDaemonSet以外のPodがないはずです。
TaskBoardがサービスを継続していることを確認します。
[Execution User: developer]
GATEWAY_PORT=$(kubectl get svc -n envoy-gateway-system envoy-default-taskboard-gateway -o jsonpath='{.spec.ports[0].nodePort}')
curl -s http://localhost:${GATEWAY_PORT}/api/tasks | python3 -m json.tool
タスク一覧が正常に返されれば、PDBによる保護が正しく機能しています。Nodeメンテナンス中もサービスが継続できました。
この体験を振り返ってください。第2回の基本設計でPDBのminAvailable: 1を設計し、第5回の構築でkubectl applyしました。あの時点では「PDBがあるとNode drainで効果がある」という知識だけでした。本回で実際にdrainを実行し、PDBが段階的な退避を制御してサービスを守る様子を体験しました。設計→構築→運用、このライフサイクルがつながった瞬間です。
8.7.4 uncordonでNodeを復帰させる
メンテナンス作業が完了したと想定して、Nodeを復帰させます。
[Execution User: developer]
kubectl uncordon k8s-applied-worker
node/k8s-applied-worker uncordoned
[Execution User: developer]
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-applied-control-plane Ready control-plane ... v1.32.x
k8s-applied-worker Ready <none> ... v1.32.x
k8s-applied-worker2 Ready <none> ... v1.32.x
k8s-applied-worker3 Ready <none> ... v1.32.x
SchedulingDisabledが消え、Nodeが通常状態に復帰しました。ただし、uncordon後に既存のPodが自動的にこのNodeに戻ってくるわけではありません。新しいPodがスケジュールされるときに、このNodeも候補に含まれるようになるだけです。Podの再配置を促したい場合は、手動でPodを削除してDeploymentに再作成させるか、次のスケールイベントを待ちます。
[Execution User: developer]
# Node復帰後のPod配置状況を確認
kubectl get pods -n app -o wide
kubectl get pods -n db -o wide
変更管理手順書 — 4. ノードメンテナンス手順
4.1 cordon → drain → 作業 → uncordon
①kubectl get pods -A -o wideでメンテナンス対象Nodeの配置状況を事前確認
②kubectl cordon <node>で新規配置を停止
③kubectl drain <node> --ignore-daemonsets --delete-emptydir-dataでPodを退避
④ drain完了後、TaskBoardの動作確認(APIレスポンス確認)
⑤ メンテナンス作業を実施(OSパッチ、Docker更新等)
⑥kubectl uncordon <node>でNodeを復帰
⑦ Node復帰後のPod配置状況を確認4.2 PDBによる安全性確保
Nginx、TaskBoard APIにはPDB(minAvailable: 1)が設定済み。drain時にPDBが段階的退避を制御し、サービスの可用性を維持する。MySQLはreplicas: 1のためPDB非設定。MySQLが配置されたNodeのdrainではDB一時停止が発生するため、メンテナンスウィンドウを設定すること。
8.8 この回のまとめ
8.8.1 成果物の確認 — 変更管理手順書 + 完成版Helmチャート
本回で作成した成果物を確認します。
成果物 1: 変更管理手順書
├── 1. アプリケーション更新手順
│ ├── 1.1 コード変更 → ビルド → イメージ作成
│ ├── 1.2 デプロイ手順(dry-run → diff → apply → 検証)
│ └── 1.3 ロールバック手順
├── 2. 設定変更手順
│ ├── 2.1 ConfigMap変更と反映
│ ├── 2.2 Secret変更と反映
│ └── 2.3 反映確認手順
├── 3. Helmによる変更管理
│ ├── 3.1 helm upgrade手順
│ ├── 3.2 helm rollback手順
│ └── 3.3 helm historyによる履歴管理
└── 4. ノードメンテナンス手順
├── 4.1 cordon → drain → 作業 → uncordon
└── 4.2 PDBによる安全性確保
成果物 2: 完成版Helmチャート
~/k8s-production/taskboard/
├── Chart.yaml
├── values.yaml # デフォルト値(prod相当)
├── values-dev.yaml # 開発環境オーバーライド
├── values-prod.yaml # 本番環境オーバーライド
└── templates/ # 全17テンプレート + NOTES.txt
変更管理手順書は、他のエンジニアがこの手順を読んで同じ操作を実行できるレベルで書いています。Helmチャートはvalues.yamlの値を切り替えるだけでdev/prod環境の差分を管理でき、helm historyで変更履歴を一元管理できます。
8.8.2 運用フェーズの振り返り(第7回〜第8回)
運用フェーズの2回を振り返ります。
| 回 | テーマ | 成果物 | 焦点 |
|---|---|---|---|
| 第7回 | 運用設計 | 運用設計書 | 「どう動くべきか」の設計(監視・スケーリング・デプロイ戦略・バックアップ) |
| 第8回 | 日常運用 | 変更管理手順書 + Helmチャート | 「日々の変更をどう回すか」の実務(更新・設定変更・Helm運用・メンテナンス) |
第7回は「仕組みの設計」、第8回は「オペレーションの実務」でした。設計書と手順書が揃ったことで、TaskBoardの日常運用を回すための文書基盤が完成しています。
これで運用フェーズは完了です。設計フェーズ(第1〜3回)で白紙から設計書を書き、構築フェーズ(第4〜6回)で設計書通りにデプロイし、運用フェーズ(第7〜8回)で運用設計書と変更管理手順書を整備しました。ライフサイクルの「平時」の一周が完了したことになります。
8.8.3 次回予告 — 障害対応、シリーズのクライマックスへ
次回(第9回)は障害対応です。シリーズのクライマックスです。
TaskBoardを意図的に壊します。アプリのクラッシュ(CrashLoopBackOff)、メモリ枯渇(OOMKilled)、Node障害、ネットワーク障害、ストレージ障害、設定ミス——6つの障害パターンを注入し、切り分け・復旧・再発防止のプロセスを実践します。
本回までに整備した運用設計書と変更管理手順書は、障害対応の場面でも活用されます。「バックアップからのリストア」は第7回で手順化済みです。「ロールバック」は本回で手順化しました。第9回では、これらの手順を「障害復旧」の文脈で実行することになります。
平時の運用が終わり、有事の対応へ。設計・構築・運用で積み上げてきたものが試されます。
AIコラム — Helm化の相談、values.yaml設計
TaskBoard全体のHelm化は、テンプレートファイルが17個、values.yamlのパラメータが数十項目に及ぶ作業でした。この規模のHelmチャートを設計するとき、AIに相談すると効率的です。
たとえば、こんなプロンプトが有効です。
TaskBoardのHelmチャートを作りたい。以下のコンポーネントがある。
– Nginx (Deployment, replicas: 2, port: 8080)
– TaskBoard API (Deployment, replicas: 2, Payara Micro, port: 8080)
– MySQL (StatefulSet, replicas: 1, port: 3306)
– DB初期化 (Job)
– DBバックアップ (CronJob)
– ログ収集 (DaemonSet)values.yamlに切り出すべきパラメータと、テンプレートにハードコードすべきパラメータを提案してください。
AIは「replicas、image.tag、resources、HPA閾値はvalues.yamlへ」「ラベル、ポート番号、Probeパスはテンプレートに固定」といった提案を返してくれます。これは本回で実際に採用した設計と概ね一致するはずです。
ただし、AIの提案をそのまま採用するのではなく、自分の環境の要件に照らして取捨選択してください。たとえば、AIは「SecurityContextもvalues.yamlに切り出すべき」と提案するかもしれません。デバッグ時にrootで実行したいケースを想定した合理的な提案です。しかし、セキュリティ要件が厳しい環境では「SecurityContextは固定し、values.yamlからの変更を許可しない」という判断もあり得ます。切り出すべきかどうかは、チームの運用方針次第です。
AIにhelm templateの出力結果を渡して「このテンプレートとvalues.yamlの整合性をチェックしてほしい」と依頼するのも効果的です。変数名のタイポや、values.yamlの階層構造のずれを素早く検出してくれます。ただし、最終的な検証は必ずhelm templateコマンドで行ってください。helm templateはテンプレートエンジンが実際にレンダリングした結果を返すため、AIのレビューより確実です。
