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

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("/api/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とします。

@Path"/api/version"である点に注意してください。第6回で設定したHTTPRouteのURLRewriteにより、外部からの/api/versionはPayara Microには/taskboard-api/api/versionとして到達します。コンテキストルート/taskboard-apiを除いた残りのパスがJAX-RSのルーティング対象になるため、@Pathには"/api/version"を指定します。既存のTaskResource@Path("/api/tasks")で動作しているのと同じパターンです。

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: 新バージョンの動作確認

Gateway API経由でアクセスします。第6回と同様にkubectl port-forwardでGatewayにアクセスする方法を使います。port 8080が既存のプロセスで占有されている場合に備え、先にクリーンアップします。

[Execution User: developer]

# 既存のport-forwardプロセスがあれば停止
pkill -f "port-forward.*8080:80" 2>/dev/null || true
sleep 1

# GatewayのService名を取得してport-forward開始
export GATEWAY_SVC=$(kubectl get svc -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=taskboard-gateway \
  -o jsonpath='{.items[0].metadata.name}')
echo "Gateway Service: $GATEWAY_SVC"
kubectl port-forward -n envoy-gateway-system svc/$GATEWAY_SVC 8080:80 &
sleep 2
Gateway Service: envoy-app-taskboard-gateway-xxxxxxxxxx-xxxxx
Forwarding from 127.0.0.1:8080 -> 10080
Forwarding from [::1]:8080 -> 10080

Forwarding from...が表示されればport-forwardは正常に起動しています。表示されずにプロンプトが戻ってきた場合は、GATEWAY_SVCの値が空でないことをecho $GATEWAY_SVCで確認してください。

[Execution User: developer]

# 新しいエンドポイントにアクセス
curl -s http://localhost:8080/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://localhost:8080/api/tasks | python3 -m json.tool --no-ensure-ascii

既存のタスク一覧が返されれば、デグレは発生していません。これで安全なデプロイフローの全5ステップが完了しました。

8.3.4 ロールバック判断と実行

デプロイ後に問題が発覚した場合のロールバック手順を確認します。第7回の運用設計書では「デプロイ後5分以内に異常を検知した場合は即座にロールバック」と定めました。

まず、ロールアウト履歴を確認します。

[Execution User: developer]

kubectl rollout history deployment/taskboard-api -n app
deployment.apps/taskboard-api
REVISION  CHANGE-CAUSE
5         <none>
6         <none>
7         <none>
8         <none>
9         <none>

リビジョンが複数表示されています。これは第7回の運用設計検証でローリングアップデートのテストやrollout restartを実行したためです。最新のリビジョン(REVISION 9)が今回デプロイした2.1.0、その直前(REVISION 8)が旧バージョンの2.0.0です。kubectl rollout undoをリビジョン番号なしで実行すると、直前のリビジョンに自動的にロールバックされます。

[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のserver_tokensディレクティブをoffに設定する変更を題材にします。レスポンスヘッダーからNginxのバージョン情報を非表示にするセキュリティ強化です。

まず、変更前のConfigMapをバックアップします。

[Execution User: developer]

# 変更前のConfigMapをバックアップ
kubectl get configmap nginx-config -n app -o yaml > /tmp/nginx-configmap-backup.yaml

ConfigMapを更新します。http { の直後に server_tokens off; を追加します。ポイントはjsonpathdataフィールドの内容だけを取り出してから編集することです。-o yamlで全体を取得すると、last-applied-configurationアノテーション内にも同じ設定が含まれており、sedが意図しない箇所にもマッチしてしまいます。

[Execution User: developer]

# server_tokens off; を http {} ブロックの先頭に追加して適用
CURRENT=$(kubectl get configmap nginx-config -n app -o jsonpath='{.data.nginx\.conf}')
UPDATED=$(echo "$CURRENT" | sed '0,/http {/{s/http {/http {\n        server_tokens off;/}')
kubectl create configmap nginx-config -n app \
  --from-file=nginx.conf=<(echo "$UPDATED") \
  --dry-run=client -o yaml | \
  kubectl apply -f -
configmap/nginx-config configured

configuredと表示されました。ConfigMapは更新されています。では、Pod内の設定ファイルに反映されているか確認しましょう。

[Execution User: developer]

# Nginx Podの中でマウントされた設定ファイルを確認(server_tokens off; が含まれるか)
NGINX_POD=$(kubectl get pods -n app -l component=frontend -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n app $NGINX_POD -- grep 'server_tokens' /etc/nginx/nginx.conf
command terminated with exit code 1

command terminated with exit code 1はエラーではありません。grepはマッチする行が見つからなかった場合に終了コード1を返し、kubectl execがそれを表示しているだけです。ConfigMapは更新済みですが、subPathでマウントしているため、Pod内の設定ファイルにはまだ反映されていません。これは重要な注意点です。ボリュームマウント(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 -- grep 'server_tokens' /etc/nginx/nginx.conf
        server_tokens off;

今度はserver_tokens off;が表示されました。rollout restartでPodが再作成され、最新のConfigMapの内容が反映されています。

変更管理手順書 — 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
│   ├── mysql-secret-app.yaml
│   ├── nginx-configmap.yaml
│   ├── db-init-job.yaml
│   ├── db-backup-cronjob.yaml
│   ├── log-collector-daemonset.yaml
│   └── NOTES.txt                  # helm install後の案内メッセージ
└── .helmignore

応用編のフロントエンドチャート(5ファイル)から、全コンポーネント対応の18テンプレート + 4設定ファイルの構成に拡張します。mysql-secret.yamlとmysql-secret-app.yamlの2つがある理由は、SecretがNamespaceを跨いで参照できないためです(第5回で確認済み)。db Namespace用(MySQL StatefulSetが参照)とapp Namespace用(TaskBoard API Deploymentが参照)をそれぞれ作成します。

ここで設計判断が必要です。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: "taskboard-root-pass"
    database: "taskboard"
    user: "taskboard"
    password: "taskboard-pass"
  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 テンプレートの作成

テンプレートは18ファイルありますが、全文を掲載すると記事が膨大になります。代表的なテンプレート(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 }}"
          imagePullPolicy: Never
          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: 0
            periodSeconds: 2
            timeoutSeconds: 3
            failureThreshold: 30
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
            successThreshold: 1
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            seccompProfile:
              type: RuntimeDefault
            capabilities:
              drop:
                - ALL
          env:
            - name: DB_HOST
              value: "mysql-0.mysql-headless.db.svc.cluster.local"
            - name: DB_NAME
              value: "taskboard"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: payara-config
              mountPath: /opt/payara/config
      volumes:
        - name: tmp
          emptyDir: {}
        - name: payara-config
          emptyDir: {}
EOF

values.yamlから注入される部分({{ .Values.api.xxx }})とハードコードされた部分を比較してください。replicas、image、resourcesはvalues.yamlから取得しますが、Probeのエンドポイントパス(/health/started等)、ポート番号(8080)、SecurityContext、環境変数のキー名はテンプレートに固定しています。strategyのmaxSurge: 1, maxUnavailable: 0も第7回の運用設計書で決定した値であり、環境差分ではないため固定です。volumeMountsの/tmp/opt/payara/configはPayara Microの動作に必要なディレクトリです。readOnlyRootFilesystem: trueを設定しているため、書き込みが必要なパスをemptyDirで明示的にマウントしています(第5回で構築した構成と同一です)。

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 }}"
          envFrom:
            - secretRef:
                name: mysql-secret
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql
            - name: tmp
              mountPath: /tmp
            - name: run-mysqld
              mountPath: /var/run/mysqld
          livenessProbe:
            tcpSocket:
              port: 3306
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            tcpSocket:
              port: 3306
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
            successThreshold: 1
          securityContext:
            runAsNonRoot: true
            runAsUser: 999
            allowPrivilegeEscalation: false
            seccompProfile:
              type: RuntimeDefault
            capabilities:
              drop:
                - ALL
      volumes:
        - name: tmp
          emptyDir: {}
        - name: run-mysqld
          emptyDir: {}
  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: 8080, 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エンコード)db Namespace用。Secret名、4キー(ROOT_PASSWORD, DATABASE, USER, PASSWORD)
mysql-secret-app.yamlauth.user, auth.password(base64エンコード)app Namespace用。Secret名、2キー(USER, PASSWORD)
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のパターンに従って作成してください。mysql-secret.yamlが2ファイル(db Namespace用とapp Namespace用)に分かれている点に注意してください。第5回で確認したとおり、SecretはNamespaceを跨いで参照できないため、API Deployment(app Namespace)が参照する認証情報も同じNamespaceに作成する必要があります。

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_SVC=$(kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=taskboard-gateway -o jsonpath='{.items[0].metadata.name}')
  kubectl port-forward -n envoy-gateway-system svc/$GATEWAY_SVC 8080:80 &
  curl http://localhost:8080/api/version
EOF

8.5.5 helm installでTaskBoardをデプロイする

既存のkubectlデプロイ済みリソースをHelm管理に移行します。学習環境のため、一度既存リソースを削除してからhelm installする方法を取ります。

[Execution User: developer]

# §8.3で起動したport-forwardを停止
pkill -f "port-forward.*8080:80" 2>/dev/null || true

# 既存のアプリケーションリソースを削除(基盤リソースは残す)
# 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
kubectl delete secret mysql-secret -n app

# db namespaceのワークロード
kubectl delete statefulset mysql -n db
kubectl delete service mysql-headless -n db
kubectl delete secret mysql-secret -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]

# port-forwardを再開してGateway API経由のアクセスを確認
export GATEWAY_SVC=$(kubectl get svc -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=taskboard-gateway \
  -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n envoy-gateway-system svc/$GATEWAY_SVC 8080:80 &
sleep 2

Forwarding from 127.0.0.1:8080 -> 10080が表示されればport-forwardは正常です。

[Execution User: developer]

curl -s http://localhost:8080/api/version | python3 -m json.tool

8.5.6 helm upgradeで更新を実行する

Helm管理下でのパラメータ変更を試します。--setフラグでvalues.yamlの値を一時的に上書きし、helm upgradeで適用します。ここではNginxのreplicas数を3に増やしてみます。

[Execution User: developer]

# --setでvalues.yamlの値を一時的に上書きしてアップグレード
helm upgrade taskboard ~/k8s-production/taskboard \
  --set nginx.replicaCount=3 \
  -n app

--setフラグでvalues.yamlの値を一時的に上書きできます。今回はNginxのreplicas数だけを変更しましたが、イメージタグの変更(--set api.image.tag="2.2.0")でもローリングアップデートが実行されます。

[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 replicas12
Nginx CPU requests/limits25m / 100m50m / 200m
Nginx Memory requests/limits32Mi / 64Mi64Mi / 128Mi
Nginx HPA無効有効(min:2, max:6)
Nginx PDB無効有効(minAvailable:1)
API replicas12
API CPU requests/limits100m / 300m200m / 500m
API Memory requests/limits256Mi / 384Mi384Mi / 512Mi
API HPA無効有効(min:2, max:4)
API PDB無効有効(minAvailable:1)
DB Backup Schedule5分間隔毎日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 --reset-values -n app

--reset-valuesを付けてデフォルトのvalues.yaml(prod相当)に戻します。--reset-valuesがないと、前回のhelm upgradeで使ったvalues(dev環境の値)がそのまま引き継がれるため、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 --reset-values -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を確認し、リビジョン番号と変更内容を記録する

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-daemonsetsDaemonSet管理のPodを無視する。DaemonSetはNode固有のPodであり、退避させる必要がない
--delete-emptydir-dataemptyDirボリュームを持つ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]

# drainでport-forwardが切れている可能性があるため、再起動
pkill -f "port-forward.*8080:80" 2>/dev/null || true
sleep 1
export GATEWAY_SVC=$(kubectl get svc -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=taskboard-gateway \
  -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n envoy-gateway-system svc/$GATEWAY_SVC 8080:80 &
sleep 2

[Execution User: developer]

curl -s http://localhost:8080/api/tasks | python3 -m json.tool --no-ensure-ascii

タスク一覧が正常に返されれば、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

# port-forwardを停止
pkill -f "port-forward.*8080:80" 2>/dev/null || true

変更管理手順書 — 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/                     # 全18テンプレート + 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化は、テンプレートファイルが18個、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のレビューより確実です。