---
id: devops-helm-deep
title: Helm 깊이 — Chart / Templating / Hook
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [devops, helm, kubernetes, vibe-coding]
tech_stack: { language: "YAML / Go template", applicable_to: ["DevOps"] }
applied_in: []
aliases: [Helm chart, values.yaml, template, hook, dependency, helmfile]
---

# Helm Deep

> Kubernetes package manager. **Chart = template + values**. 환경별 다른 values + dependency + hook + rollback. 큰 프로덕션 K8s 의 표준.

## 📖 핵심 개념
- Chart: package (templates / values / metadata).
- Values: 환경별 config.
- Template: Go template + Helm functions.
- Release: cluster 안 deployed instance.

## 💻 코드 패턴

### Chart 구조
```
my-app/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
├── values-dev.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── serviceaccount.yaml
│   ├── _helpers.tpl
│   └── tests/
│       └── connection-test.yaml
└── charts/                      # dependency
```

### Chart.yaml
```yaml
apiVersion: v2
name: my-app
description: Acme API service
version: 1.0.0       # chart version
appVersion: "1.5.0"  # app version
type: application

dependencies:
  - name: postgresql
    version: 13.2.0
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled
  - name: redis
    version: 18.0.0
    repository: https://charts.bitnami.com/bitnami
```

### values.yaml (default)
```yaml
replicaCount: 2

image:
  repository: ghcr.io/myorg/my-app
  tag: ""  # default = appVersion
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: api.example.com

resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

postgresql:
  enabled: true
  auth:
    username: app
    database: app

env:
  NODE_ENV: production
```

### values-prod.yaml (override)
```yaml
replicaCount: 5

resources:
  requests:
    cpu: 500m
    memory: 1Gi
  limits:
    cpu: 2
    memory: 4Gi

postgresql:
  enabled: false  # External RDS

env:
  DATABASE_URL: postgresql://prod...
```

### Deployment template
```yaml
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          env:
            {{- range $k, $v := .Values.env }}
            - name: {{ $k }}
              value: {{ $v | quote }}
            {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080
```

### Helpers (_helpers.tpl)
```yaml
{{/* Common labels */}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/* Selector labels */}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/* Fullname */}}
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
```

### 명령
```bash
# Render local
helm template my-app . -f values-prod.yaml

# Lint
helm lint .

# Install
helm install my-app . -f values-prod.yaml -n prod

# Upgrade
helm upgrade my-app . -f values-prod.yaml -n prod

# Diff (변경 미리)
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade my-app . -f values-prod.yaml -n prod

# Rollback
helm rollback my-app 5 -n prod  # revision 5

# Uninstall
helm uninstall my-app -n prod
```

### Conditional render
```yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}

{{- if .Values.podAnnotations }}
annotations:
  {{- toYaml .Values.podAnnotations | nindent 4 }}
{{- end }}
```

### Loop
```yaml
spec:
  template:
    spec:
      containers:
        {{- range .Values.sidecars }}
        - name: {{ .name }}
          image: {{ .image }}
        {{- end }}
```

### Secret (sealed / external)
```yaml
# templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "my-app.fullname" . }}
type: Opaque
stringData:
  {{- range $k, $v := .Values.secrets }}
  {{ $k }}: {{ $v | quote }}
  {{- end }}
```

⚠️ Secret in values.yaml prod = bad. Sealed Secrets / External Secrets Operator.

### Hooks (lifecycle)
```yaml
# Pre-install / pre-upgrade — DB migration
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-app.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: ...
          command: ["yarn", "migrate:up"]
      restartPolicy: Never
```

→ Helm install / upgrade 전 자동 migration.

### Test
```yaml
# templates/tests/connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ .Release.Name }}-test-connection"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget', '{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/healthz']
  restartPolicy: Never
```

```bash
helm test my-app
```

### Dependency
```yaml
# Chart.yaml
dependencies:
  - name: postgresql
    version: ~13.0.0
    repository: oci://registry-1.docker.io/bitnamicharts
```

```bash
helm dependency update
helm install my-app . -f values.yaml
```

→ Postgres 자동 같이 install.

### Helmfile (multiple chart)
```yaml
# helmfile.yaml
releases:
  - name: my-app
    namespace: prod
    chart: ./charts/my-app
    values: [values-prod.yaml]
  
  - name: monitoring
    namespace: monitoring
    chart: prometheus-community/kube-prometheus-stack
    values: [monitoring-values.yaml]
```

```bash
helmfile sync
helmfile diff
```

→ 여러 chart 한 번에.

### Multi-environment
```
values.yaml         # baseline / dev
values-staging.yaml # staging
values-prod.yaml    # prod

helm upgrade my-app . -f values-staging.yaml -n staging
helm upgrade my-app . -f values-prod.yaml -n prod
```

### Chart museum / OCI registry
```bash
# Push to OCI
helm package .
helm push my-app-1.0.0.tgz oci://registry.example.com/charts

# Install from OCI
helm install my-app oci://registry.example.com/charts/my-app --version 1.0.0
```

### Sub-chart override
```yaml
# 자식 chart 의 values
postgresql:
  primary:
    persistence:
      size: 100Gi
  auth:
    database: app
```

### Common functions
```yaml
{{ .Values.x | default "fallback" }}
{{ .Values.x | quote }}
{{ .Values.x | nindent 4 }}
{{ tpl .Values.x . }}              # template within value
{{ include "common.template" . }}
{{ required "x is required" .Values.x }}
{{ printf "%s-%s" .a .b }}
{{ toYaml .Values.complex | nindent 4 }}
{{ b64enc "secret" }}
{{ randAlphaNum 32 }}
```

### Values schema validation
```json
// values.schema.json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "properties": {
    "replicaCount": { "type": "integer", "minimum": 1 },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    }
  },
  "required": ["replicaCount"]
}
```

→ helm install 시 자동 검증.

### CI
```yaml
- run: helm lint .
- run: helm template . -f values-prod.yaml | kubectl --dry-run=server apply -f -
- run: helm upgrade --install my-app . -f values-prod.yaml -n prod --atomic --timeout 5m
```

### --atomic flag
```bash
helm upgrade --install --atomic
```

→ 실패 시 자동 rollback. 안전.

### Chart vs Kustomize
```
Helm:        Template engine. Complex logic.
Kustomize:   Overlay (patch) — declarative, no logic.
KCL / CUE:   New schema lang.

→ Helm 가 가장 인기. Kustomize 가 단순.
```

### ArgoCD + Helm (GitOps)
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  source:
    repoURL: https://github.com/myorg/charts
    chart: my-app
    targetRevision: 1.0.0
    helm:
      valueFiles: [values-prod.yaml]
  destination:
    server: https://kubernetes.default.svc
    namespace: prod
```

→ Git = truth. Argo 가 sync.

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| K8s app 배포 | Helm |
| 단순 / overlay | Kustomize |
| Multi-cluster / GitOps | ArgoCD + Helm |
| 매우 dynamic | Helm + Helmfile |
| 사내 chart 공유 | OCI registry |
| Public chart | Artifact Hub |

## ❌ 안티패턴
- **Secret in values.yaml + git**: leak.
- **--atomic 없이 prod**: 실패 시 broken state.
- **Chart version + appVersion 같음**: chart 변경도 app version bump 필요.
- **Single huge values.yaml**: 분리.
- **Hook 가 idempotent X**: 재시도 시 깨짐.
- **No values.schema**: 잘못된 values 통과.
- **모든 env 같은 chart**: dev / prod 차이 안 됨.

## 🤖 LLM 활용 힌트
- Chart + values per env + atomic upgrade.
- Sealed Secrets / External Secrets.
- ArgoCD GitOps.
- Helmfile 여러 chart.

## 🔗 관련 문서
- [[DevOps_Kubernetes_Basics]]
- [[DevOps_ArgoCD_GitOps]]
- [[DevOps_Service_Mesh_Deep]]
