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

Kubernetes応用編 第09回

Kubernetes応用編 第09回
マニフェストのパッケージ管理 — Helm

9.1 はじめに

前回(第8回)では、HPA(Horizontal Pod Autoscaler)による自動スケーリングとProbeの詳細設計を扱いました。TaskBoardのフロントエンド(Nginx)とAPI(Payara Micro)にHPAを適用し、負荷に応じてPod数が自動で増減する仕組みを実装しています。また、Payara Microの起動時間(15〜20秒)を活かして「startupProbeがないとどうなるか」を身体で理解し、各Probeパラメータの効果を体験しました。

現在のTaskBoardの状態を確認しておきましょう。

[app Namespace]
  Nginx (Deployment, replicas: 2) + Service (ClusterIP, targetPort: 8080)
    ★ HPA適用(CPU 70%, min:2, max:6)
    SecurityContext適用済み
  TaskBoard API (Deployment, replicas: 2, MySQL接続版) + Service (ClusterIP)
    ★ HPA適用(CPU 70%, min:2, max:4)
    ★ Probe詳細設定(startup / liveness / readiness)
    ★ カスタムHealthCheck(DB接続確認付き)
    ★ イメージをtaskboard-api:3.0.0に更新
    SecurityContext適用済み
  Gateway (taskboard-gateway) + HTTPRoute (taskboard-route)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=restricted

[db Namespace]
  MySQL (StatefulSet, replicas: 1) + Headless Service + PVC
  + DB初期化Job(Completed)
  + DBバックアップCronJob(稼働中)
  + Secret(MySQL認証情報)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=baseline

[monitoring Namespace]
  ログ収集DaemonSet(全Worker Nodeに配置)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み

[クラスタ全体]
  Calico CNI
  Metrics Server 稼働中
  Gateway API 稼働中(/ → Nginx, /api → TaskBoard API)
  マルチノード構成(CP 1 + Worker 3)

セキュリティ、ネットワーク制御、自動スケーリング、ヘルスチェック。本番運用に必要な武器はほぼ揃いました。しかし、1つだけ見て見ぬふりをしてきた課題があります。

第1回から第8回までの間に、~/k8s-applied/ディレクトリには大量のYAMLファイルが積み重なっています。Namespace定義、RBAC、Deployment、Service、StatefulSet、NetworkPolicy、HPA……。1ファイルずつkubectl applyしてきたからこそ仕組みを理解できましたが、これを日常の運用で毎回手作業で適用し続けるのは現実的でしょうか。「開発環境ではreplicas: 1、本番環境ではreplicas: 3」としたいとき、YAMLファイルをコピーして書き換えるのでしょうか。

BeforeAfter
素のYAMLを直接kubectl applyしている。ファイルが増えて全体像が見えにくいHelmチャートの構造を理解し、TaskBoardのフロントエンドマニフェストをHelm化できる

応用編の最終回となる今回は、Kubernetesマニフェストのパッケージ管理ツール「Helm」を導入します。TaskBoardのフロントエンド(Nginx)を題材にHelm化し、values.yamlで環境差分を吸収する仕組みを体験します。なお、TaskBoard全体のHelm化は実践編第8回で本格的に行います。今回は「Helmの使い方を知る」ことに集中しましょう。

9.2 VMの世界との対比 — VMテンプレートとAnsibleのパラメータ化

9.2.1 VMの世界でのテンプレート管理

VMの世界では、似たような構成のサーバーを量産するとき「テンプレート」を使います。vSphereなら「VMテンプレート」を作成し、クローンして個別設定を変更する運用です。さらに自動化が進んだ現場では、AnsibleやTerraformで構成を「コード化」し、パラメータファイルで環境差分を吸収していたはずです。

たとえばAnsibleのPlaybookでは、共通のテンプレート(Jinja2テンプレート)に対して、group_vars/dev.ymlgroup_vars/prod.ymlでパラメータを切り替えます。テンプレートには{{ web_server_count }}のような変数を埋め込み、環境ごとの変数ファイルで具体的な値を定義する構造です。

9.2.2 K8sではHelmがその役割を担う

Kubernetesの世界では、Helmがこの「テンプレート + パラメータファイル」の役割を担います。

VMの世界K8s + Helmの世界
VMテンプレート / AnsibleロールHelmチャート(テンプレートの集合)
group_vars/dev.yml, group_vars/prod.ymlvalues.yaml, values-prod.yaml
Jinja2テンプレートの変数 {{ xxx }}Goテンプレートの変数 {{ .Values.xxx }}
ansible-playbook -e @vars/prod.ymlhelm install -f values-prod.yaml
Playbookの実行履歴(Tower/AWX)Helmリリース履歴(helm history
ロールバック(前回の変数でPlaybook再実行)helm rollback

テンプレートを使い回し、パラメータファイルで環境差分を吸収するというアプローチは、VMの世界もK8sの世界も同じです。違いは、Helmがリリース(デプロイ)の履歴管理とロールバック機能を標準で備えている点です。Ansibleの場合、Tower/AWXなどの外部ツールなしでは履歴管理が手薄でしたが、Helmではコマンド一発でリリース履歴の確認と巻き戻しができます。

9.3 Helmの全体像

9.3.1 Helmの3つの概念 — Chart / Release / values.yaml

Helmを理解するには、3つの概念を押さえるだけで十分です。

Chart(チャート)は、K8sマニフェストのテンプレートと設定値をまとめたパッケージです。ディレクトリ構造として存在し、中にはテンプレートファイル(Deployment、Serviceなど)と、デフォルトの設定値(values.yaml)が含まれます。Ansibleのロールに相当します。

Release(リリース)は、Chartをクラスタにデプロイした結果です。同じChartから複数のReleaseを作成できます。たとえば「taskboard-frontend-dev」と「taskboard-frontend-prod」のように、同じChartから開発用と本番用の2つのReleaseを作成できます。helm installするたびに1つのReleaseが作られ、helm upgradeするとそのReleaseのリビジョンが増えます。

values.yamlは、テンプレートに渡すパラメータの集合です。replicas数、イメージタグ、リソース制限など、環境ごとに変わる値をここに定義します。helm installhelm upgrade時に-f values-prod.yamlのように別のvaluesファイルを渡すと、デフォルト値が上書きされます。

3つの関係を整理すると、Chart(テンプレート)+ values.yaml(パラメータ)= Release(クラスタ上のリソース)です。テンプレートは1つ。パラメータを切り替えるだけで、開発環境にも本番環境にもデプロイできます。

9.3.2 Helmをインストールする

Helmの公式インストールスクリプトを使います。

[Execution User: developer]

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

インストールが完了したら、バージョンを確認します。

[Execution User: developer]

helm version
version.BuildInfo{Version:"v3.17.1", GitCommit:"980d8ac1939e39138101364400756af2b3bb9707", GitTreeState:"clean", GoVersion:"go1.23.5"}

v3.17以上が表示されれば問題ありません。Helm v3はTiller(v2で必要だったクラスタ内のサーバーコンポーネント)が不要で、kubectlと同じkubeconfigを使ってクラスタに接続します。

9.3.3 helm create でチャートの雛形を確認する

helm createコマンドで、チャートの雛形を生成してみましょう。まず構造を理解するための参考として生成し、中身を確認します。

[Execution User: developer]

cd ~/k8s-applied
helm create sample-chart
Creating sample-chart

生成されたディレクトリ構造を確認します。

[Execution User: developer]

tree sample-chart/
sample-chart/
├── Chart.yaml          # チャートのメタデータ(名前、バージョン等)
├── values.yaml         # デフォルトのパラメータ値
├── charts/             # 依存チャートの格納先(今回は使わない)
├── templates/          # K8sマニフェストのテンプレート群
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt       # helm install後に表示されるメモ
│   ├── _helpers.tpl    # 共通のヘルパーテンプレート
│   └── tests/
│       └── test-connection.yaml
└── .helmignore         # パッケージング時の除外ファイル

重要なのは3つのファイルです。Chart.yamlがチャートの名刺、values.yamlがパラメータの集合、templates/ディレクトリ内がK8sマニフェストのテンプレートです。雛形には汎用的なDeployment、Service、Ingress等のテンプレートが含まれていますが、今回はTaskBoardのフロントエンド用にゼロから作り直します。この雛形は構造の参考として確認しただけなので削除しておきましょう。

[Execution User: developer]

rm -rf ~/k8s-applied/sample-chart

9.4 TaskBoardの現状を振り返る — マニフェストファイルの棚卸し

9.4.1 これまで作成したマニフェスト一覧を数える

Helm化の前に、現実を直視しましょう。第1回から第8回までの間に、~/k8s-applied/ディレクトリに作成したYAMLファイルを数えてみます。

[Execution User: developer]

ls ~/k8s-applied/*.yaml | wc -l
48

種別ごとに分類してみましょう。

[Execution User: developer]

echo "=== クラスタ基盤 ==="
ls ~/k8s-applied/kind-applied*.yaml ~/k8s-applied/namespace-*.yaml 2>/dev/null

echo ""
echo "=== ResourceQuota / LimitRange ==="
ls ~/k8s-applied/resourcequota-*.yaml ~/k8s-applied/limitrange-*.yaml 2>/dev/null

echo ""
echo "=== RBAC ==="
ls ~/k8s-applied/sa-*.yaml ~/k8s-applied/role-*.yaml ~/k8s-applied/rolebinding-*.yaml 2>/dev/null

echo ""
echo "=== フロントエンド(Nginx) ==="
ls ~/k8s-applied/nginx-*.yaml ~/k8s-applied/hpa-nginx.yaml 2>/dev/null

echo ""
echo "=== API(TaskBoard API) ==="
ls ~/k8s-applied/taskboard-api-*.yaml ~/k8s-applied/hpa-taskboard-api.yaml 2>/dev/null

echo ""
echo "=== DB(MySQL) ==="
ls ~/k8s-applied/mysql-*.yaml ~/k8s-applied/db-*.yaml 2>/dev/null

echo ""
echo "=== ネットワーク ==="
ls ~/k8s-applied/gateway.yaml ~/k8s-applied/httproute-*.yaml ~/k8s-applied/netpol-*.yaml 2>/dev/null

echo ""
echo "=== 監視 ==="
ls ~/k8s-applied/log-collector-*.yaml 2>/dev/null
=== クラスタ基盤 ===
kind-applied.yaml  kind-applied-calico.yaml
namespace-app.yaml  namespace-db.yaml  namespace-monitoring.yaml

=== ResourceQuota / LimitRange ===
resourcequota-app.yaml  resourcequota-db.yaml  resourcequota-monitoring.yaml
limitrange-app.yaml  limitrange-db.yaml  limitrange-monitoring.yaml

=== RBAC ===
sa-developer-app.yaml  sa-developer-db.yaml  sa-operator-app.yaml  sa-operator-db.yaml
role-developer-app.yaml  role-developer-db.yaml  role-operator-app.yaml  role-operator-db.yaml
rolebinding-developer-app.yaml  rolebinding-developer-db.yaml
rolebinding-operator-app.yaml  rolebinding-operator-db.yaml

=== フロントエンド(Nginx) ===
nginx-configmap.yaml  nginx-deployment.yaml  nginx-deployment-v2.yaml
nginx-service.yaml  nginx-service-v2.yaml  hpa-nginx.yaml

=== API(TaskBoard API) ===
taskboard-api-deployment.yaml  taskboard-api-deployment-v2.yaml
taskboard-api-deployment-v3.yaml  taskboard-api-deployment-v4.yaml
taskboard-api-deployment-v4-no-startup.yaml  taskboard-api-service.yaml
hpa-taskboard-api.yaml

=== DB(MySQL) ===
mysql-deployment-bad.yaml  mysql-statefulset.yaml  mysql-statefulset-v2.yaml
mysql-headless-service.yaml  mysql-secret.yaml  mysql-secret-app.yaml
db-init-job.yaml  db-backup-cronjob.yaml  db-backup-cronjob-fail.yaml

=== ネットワーク ===
gateway.yaml  httproute-taskboard.yaml
netpol-app-default-deny.yaml  netpol-app-allow-gateway.yaml
netpol-app-allow-api-to-db.yaml  netpol-db-default-deny.yaml
netpol-db-allow-api.yaml  netpol-db-allow-backup.yaml  netpol-db-allow-backup-egress.yaml

=== 監視 ===
log-collector-daemonset.yaml

48ファイル。しかもこの中には、学習途中のバージョン(v1、v2、v3……)や、あえて失敗させるために作ったファイル(mysql-deployment-bad.yamldb-backup-cronjob-fail.yamltaskboard-api-deployment-v4-no-startup.yaml)も含まれています。「いま稼働中のTaskBoardを再現するために、どのファイルをどの順番で適用すればいいのか」を正確に答えられるでしょうか。

9.4.2 「開発環境と本番環境でreplicasとresourcesだけ変えたい」という課題

仮に、TaskBoardを開発環境と本番環境の2つで運用することになったとしましょう。変えたいのはreplicas数とresources(CPU/メモリ制限)だけです。

項目開発環境(dev)本番環境(prod)
replicas13
CPU requests50m100m
CPU limits200m400m
Memory requests64Mi128Mi
Memory limits128Mi256Mi

素のYAMLのままだと、DeploymentファイルをコピーしてNginxのdev版とprod版を作ることになります。コピーした瞬間からメンテナンス対象が2倍に。セキュリティ設定を変更したら両方のファイルに手を入れなければなりません。ファイルが増えるほど「適用忘れ」「ファイル間の不整合」のリスクが高まります。

Helmなら、テンプレートは1つ。values.yamlの値を切り替えるだけで、環境差分を安全に管理できます。これから実際にやってみましょう。

9.5 NginxマニフェストをHelm化する

9.5.1 チャートの構造を作成する

TaskBoardのフロントエンド(Nginx)用のHelmチャートをゼロから作成します。helm createの雛形は便利ですが不要なファイルが多いため、必要なファイルだけを手作りします。

[Execution User: developer]

mkdir -p ~/k8s-applied/taskboard-frontend/templates

まず、チャートのメタデータファイル Chart.yaml を作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/Chart.yaml
apiVersion: v2
name: taskboard-frontend
description: TaskBoard Frontend (Nginx) Helm chart
type: application
version: 0.1.0       # チャート自体のバージョン
appVersion: "1.27"   # デプロイするアプリケーション(Nginx)のバージョン
EOF

apiVersion: v2はHelm v3のチャート形式です。versionはチャートのバージョン(チャートの構造やテンプレートが変わったときに上げる)、appVersionはデプロイするアプリケーションのバージョン(Nginxのバージョン)です。2つのバージョンは独立して管理します。

次に、ヘルパーテンプレート _helpers.tpl を作成します。チャート内で繰り返し使うラベルや名前の生成ロジックをここにまとめます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/templates/_helpers.tpl
{{/*
チャート名を返す
*/}}
{{- define "taskboard-frontend.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
リリース名を含む完全な名前を返す
*/}}
{{- define "taskboard-frontend.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/*
共通ラベルを返す
*/}}
{{- define "taskboard-frontend.labels" -}}
app: taskboard
component: frontend
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}

{{/*
セレクタラベルを返す(matchLabelsに使う)
*/}}
{{- define "taskboard-frontend.selectorLabels" -}}
app: taskboard
component: frontend
{{- end }}
EOF

ヘルパーテンプレートのポイントを補足します。defineでテンプレートの断片に名前を付け、includeで呼び出します。セレクタラベル(app: taskboardcomponent: frontend)は、第1回から一貫して使ってきたラベル体系をそのまま維持しています。app.kubernetes.io/managed-byhelm.sh/chartはHelmの標準的なラベルで、「このリソースはHelmで管理されている」ことを示します。

9.5.2 Deployment テンプレートを作成する

第7回で作成したnginx-deployment-v2.yamlをベースに、環境ごとに変わる値を{{ .Values.xxx }}で変数化します。ここで重要なのは「何をvalues.yamlに切り出し、何をテンプレートにハードコードするか」の判断です。

判断項目理由
values.yamlに切り出すreplicas、image(repository / tag)、resources(requests / limits)環境ごとに値が変わる。dev / prodで差分が出る典型的な項目
values.yamlに切り出すSecurityContext設定(runAsNonRoot, runAsUser等)基本は固定だが、デバッグ時に一時的に緩めたいケースがある
テンプレートにハードコードNamespace(app固定)TaskBoardのフロントエンドは必ずapp Namespaceに配置する。環境差分ではない
テンプレートにハードコードラベル体系(app: taskboard, component: frontend)サービスディスカバリの基盤。変更するとServiceやNetworkPolicyが壊れる
テンプレートにハードコードポート番号(8080固定)Nginxの設定(nginx.conf)と密結合。値だけ変えても動かない

原則として、「環境ごとに変わる値」はvalues.yamlへ、「変えるとシステムが壊れる値」はテンプレートにハードコードします。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: app
  labels:
    {{- include "taskboard-frontend.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "taskboard-frontend.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "taskboard-frontend.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: nginx
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "{{ .Values.resources.requests.cpu }}"
              memory: "{{ .Values.resources.requests.memory }}"
            limits:
              cpu: "{{ .Values.resources.limits.cpu }}"
              memory: "{{ .Values.resources.limits.memory }}"
          securityContext:
            runAsNonRoot: {{ .Values.securityContext.runAsNonRoot }}
            runAsUser: {{ .Values.securityContext.runAsUser }}
            readOnlyRootFilesystem: {{ .Values.securityContext.readOnlyRootFilesystem }}
            allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }}
            seccompProfile:
              type: {{ .Values.securityContext.seccompProfileType }}
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
              readOnly: true
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /var/cache/nginx
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config
        - name: tmp
          emptyDir: {}
        - name: cache
          emptyDir: {}
EOF

テンプレートの読み方を確認します。{{ .Values.replicaCount }}はvalues.yamlのreplicaCountの値に置き換わります。{{- include "taskboard-frontend.labels" . | nindent 4 }}は先ほど_helpers.tplで定義したラベルテンプレートを呼び出し、インデント4スペースで挿入します。nindentは改行 + インデントを行うHelmの組み込み関数です。

ハードコードした部分にも注目してください。namespace: appcontainerPort: 8080、volumeMountsの構造(nginx-config, tmp, cache)はテンプレート内に固定しています。これらは環境によって変わるべきではない値です。ConfigMap名(nginx-config)も固定です。ConfigMap自体は今回Helm化の対象外としています(実践編第8回でTaskBoard全体をHelm化する際に含めます)。

9.5.3 Service テンプレートを作成する

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: app
  labels:
    {{- include "taskboard-frontend.labels" . | nindent 4 }}
spec:
  type: ClusterIP
  selector:
    {{- include "taskboard-frontend.selectorLabels" . | nindent 4 }}
  ports:
    - port: 80
      targetPort: 8080
EOF

Serviceはシンプルです。port: 80(Service自体のポート)とtargetPort: 8080(Nginx Podへの転送先ポート)はハードコードしています。これらはHTTPRouteのbackendRefs.portやNetworkPolicyのポート指定と連動するため、values.yamlで簡単に変えられるようにすべきではありません。

9.5.4 values.yaml で環境差分を定義する

デフォルトのvalues.yaml(開発環境相当)を作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/values.yaml
# TaskBoard Frontend (Nginx) - デフォルト値(dev環境相当)

replicaCount: 1

image:
  repository: nginx
  tag: "1.27"

resources:
  requests:
    cpu: "50m"
    memory: "64Mi"
  limits:
    cpu: "200m"
    memory: "128Mi"

securityContext:
  runAsNonRoot: true
  runAsUser: 101               # nginx:1.27 の nginx ユーザー (uid=101)
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  seccompProfileType: RuntimeDefault
EOF

各値の設計根拠を整理します。

パラメータdev値根拠
replicaCount1開発環境では1レプリカで十分。リソース節約
image.repositorynginx公式Nginxイメージを使用
image.tag1.27入門編から一貫して使用しているバージョン
resources.requests.cpu50m静的ファイル配信のみ。設計書の目安通り
resources.limits.memory128MiNginxのワーカープロセスに十分な量。設計書の目安通り
securityContext.runAsUser101nginx:1.27イメージのnginxユーザー(uid=101)。第7回で確認済み

続いて、本番環境用のvaluesファイルを作成します。デフォルトのvalues.yamlとの差分だけを記載すれば、残りの値はデフォルトが使われます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-frontend/values-prod.yaml
# TaskBoard Frontend (Nginx) - 本番環境用の上書き値

replicaCount: 3

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "400m"
    memory: "256Mi"
EOF

values-prod.yamlには差分だけを書いている点に注目してください。imagesecurityContextはデフォルトのvalues.yamlの値がそのまま使われます。「差分だけを管理する」というのがHelmの環境管理のポイントです。

完成したチャートの構造を確認しましょう。

[Execution User: developer]

tree ~/k8s-applied/taskboard-frontend/
taskboard-frontend/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
    ├── _helpers.tpl
    ├── deployment.yaml
    └── service.yaml

6ファイルのシンプルなチャートです。これだけで、NginxのDeploymentとServiceを環境差分付きでデプロイできます。

9.5.5 helm install でデプロイする

Helmでデプロイする前に、まず既存のNginx DeploymentとServiceを削除します。Helmは自身が管理するリソースを追跡するため、既存のkubectl applyで作成したリソースとは別物として扱われます。既存リソースが残ったままだと名前が衝突してエラーになります。

[Execution User: developer]

# 既存のNginx DeploymentとServiceを削除(HPA、ConfigMapは残す)
kubectl delete deployment nginx -n app
kubectl delete service nginx -n app
deployment.apps "nginx" deleted
service "nginx" deleted

HPAやConfigMap、NetworkPolicyなどの関連リソースはそのまま残しています。今回Helm化するのはDeploymentとServiceだけです。HPAは既存のものがDeployment/nginxをターゲットにしているため、Helmで同名のDeploymentがデプロイされれば自動的に再接続されます。

デプロイの前に、helm templateコマンドでテンプレートのレンダリング結果を確認しましょう。実際にクラスタに何も作成せず、生成されるYAMLだけを確認できます。

[Execution User: developer]

helm template taskboard-frontend ~/k8s-applied/taskboard-frontend
---
# Source: taskboard-frontend/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: app
  labels:
    app: taskboard
    component: frontend
    app.kubernetes.io/managed-by: Helm
    helm.sh/chart: taskboard-frontend-0.1.0
spec:
  type: ClusterIP
  selector:
    app: taskboard
    component: frontend
  ports:
    - port: 80
      targetPort: 8080
---
# Source: taskboard-frontend/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: app
  labels:
    app: taskboard
    component: frontend
    app.kubernetes.io/managed-by: Helm
    helm.sh/chart: taskboard-frontend-0.1.0
spec:
  replicas: 1
  selector:
    matchLabels:
      app: taskboard
      component: frontend
  template:
    metadata:
      labels:
        app: taskboard
        component: frontend
    spec:
      containers:
        - name: nginx
          image: "nginx:1.27"
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"
          securityContext:
            runAsNonRoot: true
            runAsUser: 101
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            seccompProfile:
              type: RuntimeDefault
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
              readOnly: true
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /var/cache/nginx
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config
        - name: tmp
          emptyDir: {}
        - name: cache
          emptyDir: {}

第7回で作成したnginx-deployment-v2.yamlnginx-service-v2.yamlの内容が正しく再現されていることを確認してください。replicas: 1はdev環境のデフォルト値です。SecurityContextの設定(runAsUser: 101、seccompProfile: RuntimeDefault等)も第7回の最終状態と一致しています。

では、デプロイしましょう。

[Execution User: developer]

helm install taskboard-frontend ~/k8s-applied/taskboard-frontend -n app
NAME: taskboard-frontend
LAST DEPLOYED: Mon Jan 20 10:30:00 2026
NAMESPACE: app
STATUS: deployed
REVISION: 1

helm installの構文は、helm install [リリース名] [チャートのパス] [オプション]です。-n appはHelmがリリース情報を記録するNamespaceを指定しています(テンプレート内でNamespaceをハードコードしているため、リソースの作成先には影響しません)。

デプロイされたリソースを確認します。

[Execution User: developer]

kubectl get deployment,service,pods -n app -l component=frontend
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx   1/1     1            1           30s

NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/nginx   ClusterIP   10.96.45.123   <none>        80/TCP    30s

NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-6f8b9c7d4-m2k5j   1/1     Running   0          30s

dev環境のデフォルト値(replicas: 1)でNginxがデプロイされました。Gateway API経由でのアクセスも確認しておきましょう。

[Execution User: developer]

GATEWAY_IP=$(kubectl get gateway taskboard-gateway -n app -o jsonpath='{.status.addresses[0].value}')
curl -s -o /dev/null -w "%{http_code}" http://${GATEWAY_IP}/
200

HTTPステータス200が返りました。Helmでデプロイした Nginx が正常に動作し、Gateway API経由のルーティングも維持されています。Service名(nginx)とポート(80)が変わっていないため、HTTPRouteのbackendRefsには一切手を加える必要がありませんでした。

Helmのリリース一覧も確認しておきます。

[Execution User: developer]

helm list -n app
NAME                	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART                    	APP VERSION
taskboard-frontend  	app      	1       	2026-01-20 10:30:00.000000000 +0900 JST	deployed	taskboard-frontend-0.1.0	1.27

REVISION 1のリリースが作成されています。ここからアップグレードやロールバックを行うと、REVISIONが増えていきます。

9.6 Helmの運用サイクルを体験する

9.6.1 values.yamlの値を変更して helm upgrade する

本番環境を想定して、values-prod.yamlの値で更新してみましょう。replicasが1から3に、resourcesが増強されます。

[Execution User: developer]

helm upgrade taskboard-frontend ~/k8s-applied/taskboard-frontend \
  -f ~/k8s-applied/taskboard-frontend/values-prod.yaml \
  -n app
Release "taskboard-frontend" has been upgraded. Happy Helming!
NAME: taskboard-frontend
LAST DEPLOYED: Mon Jan 20 10:35:00 2026
NAMESPACE: app
STATUS: deployed
REVISION: 2

REVISIONが1から2に上がりました。Pod数の変化を確認します。

[Execution User: developer]

kubectl get deployment nginx -n app
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   3/3     3            3           5m

replicas: 3に変わっています。resourcesも確認します。

[Execution User: developer]

kubectl get deployment nginx -n app -o jsonpath='{.spec.template.spec.containers[0].resources}' | python3 -m json.tool
{
    "limits": {
        "cpu": "400m",
        "memory": "256Mi"
    },
    "requests": {
        "cpu": "100m",
        "memory": "128Mi"
    }
}

values-prod.yamlの値が正しく反映されています。YAMLファイルをコピーして書き換える必要はありません。テンプレートは1つのまま、valuesファイルを差し替えるだけで環境差分を管理できます。

9.6.2 問題発生を想定して helm rollback する

ここで「構築 → 破壊 → 復活」の流れを体験しましょう。イメージタグの変更をシミュレートします。

[Execution User: developer]

helm upgrade taskboard-frontend ~/k8s-applied/taskboard-frontend \
  -f ~/k8s-applied/taskboard-frontend/values-prod.yaml \
  --set image.tag="1.99-does-not-exist" \
  -n app
Release "taskboard-frontend" has been upgraded. Happy Helming!
NAME: taskboard-frontend
LAST DEPLOYED: Mon Jan 20 10:40:00 2026
NAMESPACE: app
STATUS: deployed
REVISION: 3

--setオプションを使うと、valuesファイルを変更せずにコマンドラインで値を上書きできます。ここでは存在しないイメージタグ1.99-does-not-existを指定しました。Helm自体は「マニフェストを適用した」ところまでが責務なので、STATUSはdeployedと表示されます。しかし、クラスタ上ではイメージの取得に失敗しているはずです。

[Execution User: developer]

kubectl get pods -n app -l component=frontend
NAME                     READY   STATUS             RESTARTS   AGE
nginx-5d8f9a7b3-h2k4j   0/1     ImagePullBackOff   0          30s
nginx-5d8f9a7b3-m7n9p   0/1     ImagePullBackOff   0          30s
nginx-5d8f9a7b3-r3w5x   0/1     ImagePullBackOff   0          30s
nginx-6f8b9c7d4-k8m2n   1/1     Running            0          5m
nginx-6f8b9c7d4-p4r6t   1/1     Running            0          5m
nginx-6f8b9c7d4-t2v8w   1/1     Running            0          5m

ImagePullBackOffが出ています。存在しないイメージなので当然です。Deploymentのローリングアップデートにより、旧バージョンのPodはまだ稼働中ですが、新バージョンのPodが正常起動できないためデプロイが進みません。

すぐにロールバックしましょう。

[Execution User: developer]

helm rollback taskboard-frontend 2 -n app
Rollback was a success! Happy Helming!

helm rollback [リリース名] [リビジョン番号]で、指定したリビジョンの状態にロールバックできます。リビジョン2は「values-prod.yamlを適用した正常な状態」です。

[Execution User: developer]

kubectl get pods -n app -l component=frontend
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6f8b9c7d4-k8m2n   1/1     Running   0          7m
nginx-6f8b9c7d4-p4r6t   1/1     Running   0          7m
nginx-6f8b9c7d4-t2v8w   1/1     Running   0          7m

ImagePullBackOffのPodが消え、正常なPodだけが稼働しています。helm rollback一発で復旧できました。VMの世界でスナップショットからリストアするような感覚ですが、Helmの場合は「どのリビジョンに戻すか」を履歴から選べる点がより柔軟です。

9.6.3 helm history でリリース履歴を確認する

[Execution User: developer]

helm history taskboard-frontend -n app
REVISION	UPDATED                 	STATUS    	CHART                    	APP VERSION	DESCRIPTION
1       	Mon Jan 20 10:30:00 2026	superseded	taskboard-frontend-0.1.0	1.27       	Install complete
2       	Mon Jan 20 10:35:00 2026	superseded	taskboard-frontend-0.1.0	1.27       	Upgrade complete
3       	Mon Jan 20 10:40:00 2026	superseded	taskboard-frontend-0.1.0	1.27       	Upgrade complete
4       	Mon Jan 20 10:42:00 2026	deployed  	taskboard-frontend-0.1.0	1.27       	Rollback to 2

全操作の履歴が残っています。REVISION 1がhelm install、REVISION 2がvalues-prod.yamlでのupgrade、REVISION 3が問題のあるイメージタグでのupgrade、REVISION 4がREVISION 2へのrollbackです。STATUSがdeployedなのは現在アクティブなリビジョン(4)だけで、残りはsuperseded(置き換え済み)です。

ロールバックは新しいリビジョンとして記録される点に注意してください。REVISION 2の内容を復元しましたが、「REVISION 2に戻した」のではなく「REVISION 4としてREVISION 2の状態を再適用した」という履歴になっています。巻き戻しの事実が履歴に残るため、あとから「何が起きたか」を追跡できます。

9.6.4 helm uninstall でクリーンアップする

最後に、helm uninstallでリリースを削除する手順も確認しておきましょう。ただし、このあとの9.7節のまとめで動作中のTaskBoardを確認するため、ここでは一旦devのデフォルト値で再デプロイしておきます。

まず、現在のリリースを削除します。

[Execution User: developer]

helm uninstall taskboard-frontend -n app
release "taskboard-frontend" uninstalled

helm uninstallはリリースに紐づくすべてのK8sリソース(Deployment、Service)を削除し、リリース履歴もクリアします。

[Execution User: developer]

kubectl get deployment,service -n app -l component=frontend
No resources found in app namespace.

きれいに削除されました。では、第8回終了時のreplicas: 2の状態でTaskBoardを復旧させましょう。--setでreplicaCountを2に指定します。

[Execution User: developer]

helm install taskboard-frontend ~/k8s-applied/taskboard-frontend \
  --set replicaCount=2 \
  -n app
NAME: taskboard-frontend
LAST DEPLOYED: Mon Jan 20 10:50:00 2026
NAMESPACE: app
STATUS: deployed
REVISION: 1

[Execution User: developer]

kubectl get deployment nginx -n app
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   2/2     2            2           15s

replicas: 2でNginxが稼働しています。HPAも正常に再接続されているか確認しておきましょう。

[Execution User: developer]

kubectl get hpa -n app
NAME                REFERENCE          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa           Deployment/nginx   2%/70%    2         6         2          45m
taskboard-api-hpa   Deployment/taskboard-api   6%/70%    2         4         2          40m

HPAのTARGETSに正常な値が表示されています。HPA自体はkubectl applyで作成したものがそのまま残っており、Helmでデプロイし直したDeployment/nginxを自動的に認識しています。Deployment名が同じ(nginx)であれば、HPAは作成手段(kubectl applyかhelm installか)を区別しません。

9.7 この回のまとめ — 応用編の総括

9.7.1 TaskBoardの完成形 — 全武器の装備マップ

応用編の全9回を通じて、TaskBoardに以下の武器が装備されました。

┌──────────────────────────────────────────────────────────────────┐
│                     TaskBoard 完全体                             │
│                 (応用編 第9回 終了時点)                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  [app Namespace]                                                 │
│    Nginx (Deployment, replicas: 2)                               │
│      ★ Helm化(Chart: taskboard-frontend)← 今回                │
│      + Service (ClusterIP, port:80 → targetPort:8080)            │
│      + HPA(CPU 70%, min:2, max:6)                              │
│      + SecurityContext(非root, readOnlyFS, seccomp)             │
│    TaskBoard API (Deployment, replicas: 2, MySQL接続版)          │
│      + Service (ClusterIP)                                       │
│      + HPA(CPU 70%, min:2, max:4)                              │
│      + Probe詳細設定(startup / liveness / readiness)            │
│      + カスタムHealthCheck(DB接続確認)                          │
│      + SecurityContext(非root, readOnlyFS, seccomp)             │
│    Gateway (taskboard-gateway) + HTTPRoute (taskboard-route)     │
│      / → Nginx, /api → TaskBoard API                             │
│    ResourceQuota / LimitRange                                    │
│    RBAC(developer / operator ServiceAccount)                   │
│    NetworkPolicy(デフォルト拒否 + 必要な通信のみ許可)           │
│    Pod Security Admission: warn=restricted                       │
│                                                                  │
│  [db Namespace]                                                  │
│    MySQL (StatefulSet, replicas: 1)                               │
│      + Headless Service + PVC(永続ストレージ)                   │
│      + DB初期化Job(Completed)                                   │
│      + DBバックアップCronJob(稼働中)                            │
│      + Secret(MySQL認証情報)                                    │
│    ResourceQuota / LimitRange                                    │
│    RBAC                                                          │
│    NetworkPolicy(API→DBのみ許可、バックアップCronJob考慮済み)   │
│    Pod Security Admission: warn=baseline                         │
│                                                                  │
│  [monitoring Namespace]                                          │
│    ログ収集DaemonSet(全Worker Nodeに配置)                       │
│    ResourceQuota / LimitRange                                    │
│    RBAC                                                          │
│                                                                  │
│  [クラスタ全体]                                                   │
│    Calico CNI                                                    │
│    Metrics Server                                                │
│    Gateway API(Envoy Gateway)                                  │
│    マルチノード構成(CP 1 + Worker 3)                            │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

第1回で空のクラスタにNamespaceを切ったところから始まり、9回を経てここまでたどり着きました。セキュリティ、ネットワーク制御、自動スケーリング、ヘルスチェック、パッケージ管理。本番で戦うための武器が一通り揃っています。

9.7.2 Helm設計の判断基準 — いつ使う / いつ使わない

判断ケース
Helmを使うべき複数環境(dev / staging / prod)で同じアプリをデプロイする。マニフェストが5ファイル以上になりパッケージとして管理したい。リリース履歴とロールバックが必要
素のYAMLで十分環境が1つだけで環境差分がない。マニフェストが1〜2ファイルの小規模構成。学習やテスト目的で一時的に作成する
values.yamlに切り出すべき環境ごとに値が変わる項目(replicas、resources、image tag)。デバッグ時に一時的に変更したい設定
テンプレートにハードコードすべき変えるとシステムが壊れる値(ポート番号、ラベル体系、Namespace)。他のリソース(NetworkPolicy、HTTPRoute等)と連動する設定

🔧 トラブルシュートTips

helm installで「cannot re-use a name that is still in use」エラーが出たら、同名のリリースがすでに存在しています。helm list -n appで確認し、不要ならhelm uninstallしてから再実行してください。

helm templateで生成されるYAMLが期待と異なる場合は、values.yamlの階層構造(インデント)を確認してください。YAMLではインデントがずれると別のキーとして解釈されます。

helm upgrade後にPodがImagePullBackOffになった場合は、今回体験したようにhelm rollbackで即座に復旧できます。まず復旧してから原因調査するのが鉄則です。

9.7.3 応用編で手に入れた武器の一覧

応用編全9回で学んだ武器を一覧にまとめます。

テーマ手に入れた武器TaskBoardへの効果
第1回環境構築とNamespace設計Namespace / ResourceQuota / LimitRange / Metrics Server環境分離とリソース制限。マルチノードクラスタ上でTaskBoardが稼働開始
第2回RBACアクセス制御ServiceAccount / Role / RoleBinding「誰が何をできるか」の制御。developer / operatorの権限分離
第3回StatefulSetでDB運用StatefulSet / Headless Service / PVCMySQLの永続化とステートフル運用。APIをMySQL接続版に更新
第4回DaemonSet / Job / CronJobDaemonSet / Job / CronJobDB初期化、定期バックアップ、全ノードログ収集
第5回Gateway APIルーティングGateway / HTTPRoute外部からの L7 ルーティング(パスベース振り分け)
第6回NetworkPolicyNetworkPolicy(デフォルト拒否 + ホワイトリスト)Pod間通信の最小権限制御。バックアップCronJobの通信も考慮
第7回SecurityContext / PSSSecurityContext / Pod Security Standardsコンテナの非root化、FS読み取り専用化、権限昇格禁止
第8回HPA / Probe詳細HPA / startup / liveness / readiness Probe自動スケーリングとヘルスチェック。MicroProfile Health活用
第9回HelmChart / Release / values.yamlフロントエンドのHelm化。環境差分のパラメータ管理

9.7.4 実践編への招待 — 「武器の使い方」から「武器の選び方」へ

応用編ではTaskBoardに1つずつ武器を追加してきました。「Namespaceはこう使う」「StatefulSetはこう動く」「NetworkPolicyはこう書く」。各回で1つの武器の使い方を覚え、TaskBoardに装備していくスタイルです。

しかし、実際のプロジェクトではこの順番で物事は進みません。まず要件があり、要件からリソースを選定し、設計書を書き、手順に沿って構築し、運用設計を整え、障害に対処する。この「ライフサイクル」を一周回す体験がなければ、武器は持っているだけで使いこなせません。

実践編では、応用編で作り上げたTaskBoardを一度すべて削除します。そして、同じTaskBoardの要件を白紙から提示します。今度は「結果的に動くもの」ではなく、「最初から全体を設計して作る」プロセスを踏みます。

構成図を描き、基本設計書を書き、マニフェストの全パラメータを根拠とともに決め、設計書通りに段階デプロイし、運用設計を整え、障害に対処する。応用編で手に入れた武器を「いつ・なぜ選ぶか」を設計判断する。それが実践編の目標です。

なお、今回はフロントエンド(Nginx)のみをHelm化しましたが、実践編第8回ではTaskBoard全体(API、DB、NetworkPolicy、RBAC等を含む)を1つのHelmチャートにまとめます。values.yamlで環境差分を管理し、helm upgradeで安全に変更を適用する日常運用のワークフローを実践します。

応用編の9回、お疲れさまでした。「本番で戦える武器」は揃いました。実践編では、その武器を使いこなすための「現場力」を身につけましょう。

AIコラム — HelmチャートのレビューをAIに依頼する

Helmチャートを作成したあと、「テンプレートの構文に誤りがないか」「values.yamlとテンプレートの変数名が一致しているか」をセルフチェックするのは骨が折れます。この確認作業はAIが得意とする領域です。

AIに「以下のHelmチャート(Chart.yaml、values.yaml、templates/deployment.yaml)をレビューしてほしい。テンプレート変数とvalues.yamlのキーの整合性、Goテンプレート構文の正確性、K8sマニフェストとしての妥当性を確認して」と依頼すると、変数名のタイポや構文エラーを短時間で指摘してくれます。

ただし、AIのレビュー結果は必ずhelm templateで実際にレンダリングして検証してください。AIが「正しい」と言ったテンプレートでも、インデントのずれや型の不一致でエラーになることはあります。helm templateは嘘をつきませんが、AIは間違えることがあります。