この記事の要点
• HelmでKubernetesマニフェストをテンプレート化し再利用性を高める
• values.yamlによる環境別設定の切り替えとデプロイの自動化
• Chart MuseumやHelm Hooksでリリースライフサイクルを制御する
Helmとは
Helmは、Kubernetesのためのパッケージマネージャーです。複数のKubernetesマニフェストをChartとしてまとめ、バージョン管理・デプロイ・ロールバックを一元的に管理できます。
Helmが解決する課題
| 課題 | Helmによる解決 |
|---|---|
| マニフェストの重複 | テンプレート化で共通部分を抽出 |
| 環境ごとの設定変更 | values.yamlで環境別パラメータ管理 |
| デプロイ履歴の追跡 | リリース履歴を自動記録 |
| ロールバック | helm rollbackで即座に前バージョンに戻す |
ポイント: Helmは宣言的なテンプレートエンジンであり、変数・条件分岐・ループをサポートします。
Helmのインストール
# macOS
brew install helm
# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Windows
choco install kubernetes-helm
# バージョン確認
helm version --short
実践メモ: Helm 3ではTiller(サーバーコンポーネント)が廃止され、クライアントのみで動作します。
Chart構造
ディレクトリレイアウト
my-app/
├── Chart.yaml # Chart定義(名前・バージョン)
├── values.yaml # デフォルト値
├── charts/ # 依存Chart
├── templates/ # Kubernetesマニフェストテンプレート
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── _helpers.tpl # テンプレートヘルパー
│ ├── NOTES.txt # インストール後に表示されるメッセージ
│ └── tests/ # Helmテスト
│ └── test-connection.yaml
└── .helmignore # パッケージ時に無視するファイル
Chart.yaml
# Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
# Chart自体のバージョン
version: 0.1.0
# アプリケーションのバージョン
appVersion: "1.0.0"
# 依存関係
dependencies:
- name: postgresql
version: 12.1.0
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
maintainers:
- name: Team Platform
email: platform@example.com
keywords:
- web
- backend
- nodejs
sources:
- https://github.com/example/my-app
テンプレート作成
Deployment
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "my-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "my-app.fullname" . }}
- secretRef:
name: {{ include "my-app.fullname" . }}
livenessProbe:
httpGet:
path: {{ .Values.livenessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.readinessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
Service
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "my-app.selectorLabels" . | nindent 4 }}
Ingress
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "my-app.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
注意: checksum/configアノテーションを使うと、ConfigMapやSecretの変更時に自動的にPodが再起動されます。
ヘルパーテンプレート
# templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
{{ include "my-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
values.yaml設計
# values.yaml
replicaCount: 2
image:
repository: myregistry.io/my-app
pullPolicy: IfNotPresent
tag: "" # デフォルトは Chart.AppVersion
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: false
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: my-app.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: my-app-tls
hosts:
- my-app.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
livenessProbe:
path: /health
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
path: /ready
initialDelaySeconds: 5
periodSeconds: 5
env:
NODE_ENV: production
LOG_LEVEL: info
postgresql:
enabled: true
auth:
username: myapp
password: changeme
database: myapp
primary:
persistence:
size: 10Gi
環境別values
# values-dev.yaml
replicaCount: 1
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
env:
NODE_ENV: development
LOG_LEVEL: debug
postgresql:
enabled: true
primary:
persistence:
size: 1Gi
# values-prod.yaml
replicaCount: 5
ingress:
enabled: true
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 50
postgresql:
enabled: false # 本番では外部RDSを使用
ポイント: -f values-prod.yamlで環境固有の設定を上書きし、DRY原則を保ちます。
Chartのデプロイ
# Chartの検証
helm lint my-app/
# テンプレートのレンダリング確認
helm template my-app ./my-app
# 特定の環境用にレンダリング
helm template my-app ./my-app -f ./my-app/values-prod.yaml
# ドライラン(実際にデプロイせずKubernetesに送信される内容を確認)
helm install my-app ./my-app --dry-run --debug
# インストール
helm install my-app ./my-app -n production --create-namespace
# 環境別インストール
helm install my-app ./my-app -f ./my-app/values-prod.yaml -n production
# アップグレード
helm upgrade my-app ./my-app -f ./my-app/values-prod.yaml -n production
# インストールまたはアップグレード(存在しなければインストール、あればアップグレード)
helm upgrade --install my-app ./my-app -f ./my-app/values-prod.yaml -n production --atomic
# ロールバック
helm rollback my-app 3 -n production
# アンインストール
helm uninstall my-app -n production
実践メモ: --atomicフラグを付けると、アップグレード失敗時に自動的に前バージョンにロールバックされます。
Helm Hooks
# templates/db-migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ include "my-app.fullname" . }}-db-migration"
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
# リリース前に実行
"helm.sh/hook": pre-upgrade,pre-install
# 重み(小さいほど先に実行)
"helm.sh/hook-weight": "0"
# 完了後に削除
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
template:
metadata:
name: "{{ include "my-app.fullname" . }}-db-migration"
spec:
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["npm", "run", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "my-app.fullname" . }}
key: database-url
Hook種類
| Hook | タイミング |
|---|---|
pre-install | リソース作成前 |
post-install | すべてのリソース作成後 |
pre-upgrade | アップグレード前 |
post-upgrade | アップグレード後 |
pre-delete | 削除前 |
post-delete | 削除後 |
pre-rollback | ロールバック前 |
post-rollback | ロールバック後 |
Chartのパッケージングと配布
# Chartのパッケージ化
helm package my-app/
# 出力: my-app-0.1.0.tgz
# インデックス生成(Chart Museum用)
helm repo index . --url https://charts.example.com
# リモートリポジトリへのプッシュ(Chart Museum)
curl --data-binary "@my-app-0.1.0.tgz" https://charts.example.com/api/charts
# OCI形式でのプッシュ(Harbor, ACR, GCRなど)
helm package my-app/
helm push my-app-0.1.0.tgz oci://registry.example.com/helm-charts
# OCI形式からのインストール
helm install my-app oci://registry.example.com/helm-charts/my-app --version 0.1.0
依存関係管理
# Chart.yaml
dependencies:
- name: redis
version: 17.3.0
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
- name: postgresql
version: 12.1.0
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
# 依存Chartのダウンロード
helm dependency update my-app/
# 依存関係の確認
helm dependency list my-app/
注意: helm dependency updateを実行するとcharts/ディレクトリに依存Chartがダウンロードされます。Gitにコミットする場合は.helmignoreに追加してください。
Chart Museum のセットアップ
# chartmuseum-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: chartmuseum
spec:
replicas: 1
selector:
matchLabels:
app: chartmuseum
template:
metadata:
labels:
app: chartmuseum
spec:
containers:
- name: chartmuseum
image: ghcr.io/helm/chartmuseum:v0.15.0
args:
- --storage=local
- --storage-local-rootdir=/charts
- --port=8080
ports:
- containerPort: 8080
volumeMounts:
- name: charts
mountPath: /charts
volumes:
- name: charts
persistentVolumeClaim:
claimName: chartmuseum-pvc
---
apiVersion: v1
kind: Service
metadata:
name: chartmuseum
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: chartmuseum
# リポジトリの追加
helm repo add my-charts https://charts.example.com
helm repo update
# Chartのインストール
helm install my-app my-charts/my-app --version 0.1.0
テスト
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "my-app.fullname" . }}-test-connection"
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "my-app.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
# テストの実行
helm test my-app -n production
# テスト結果
NAME: my-app
LAST DEPLOYED: Fri Apr 24 10:00:00 2026
NAMESPACE: production
STATUS: deployed
REVISION: 1
TEST SUITE: my-app-test-connection
Last Started: Fri Apr 24 10:00:10 2026
Last Completed: Fri Apr 24 10:00:15 2026
Phase: Succeeded