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

Kubernetes実践編 #08

【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パッチ → リブート → メンテナンスモード解除cordondrain → 作業 → uncordon
変更管理ツールVMテンプレート + Ansible + Tower/AWXHelmチャート + 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.02.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操作影響
1Secret(mysql-secret)を更新既存Podには影響なし(環境変数は起動時に固定)
2MySQL側のパスワードを変更既存の接続は維持される(セッション中)
3TaskBoard 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 edit or kubectl apply -f
③ 反映方法を選択:ボリュームマウント(subPathなし)→ 自動反映を待つ / それ以外 → rollout restart

2.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: taskboardcomponent: 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.yamlreplicaCount, image, resourcesport: 8080, SecurityContext, volumeMounts
nginx-service.yaml(なし — 全て固定)type: ClusterIP, port: 80, targetPort: 8080
nginx-hpa.yamlminReplicas, maxReplicas, targetCPU対象Deployment名
nginx-pdb.yamlminAvailableセレクタラベル
taskboard-api-service.yaml(なし)type: ClusterIP, port: 80, targetPort: 8080
taskboard-api-hpa.yamlminReplicas, maxReplicas, targetCPU対象Deployment名
taskboard-api-pdb.yamlminAvailableセレクタラベル
mysql-headless-service.yaml(なし)clusterIP: None, port: 3306
mysql-secret.yamlauth.*(base64エンコード)Secret名
nginx-configmap.yaml(なし — nginx.confは固定)nginx.confの内容
db-init-job.yamlimage初期化SQL、Secret参照
db-backup-cronjob.yamlschedule, imageバックアップコマンド
log-collector-daemonset.yamlimagevolumeMounts(/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 annotatekubectl 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ではcordondrain→作業→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-worker2k8s-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のレビューより確実です。