Helm Charts - Kubernetesアプリケーションパッケージング実践

中級 | 15分 で読める | 2026.04.24

公式ドキュメント

この記事の要点

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

関連記事

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る