Helm이란? 왜 필요한가
Helm은 Kubernetes의 패키지 매니저다. YAML 매니페스트를 템플릿화하여 환경별 설정(dev/staging/prod)을 값(values)만 바꿔 배포할 수 있다. 수십 개의 YAML 파일을 개별 관리하는 대신, 하나의 Chart로 묶어 버전 관리, 롤백, 의존성 관리를 체계적으로 수행한다.
실제 운영에서 Deployment, Service, ConfigMap, Secret, Ingress, HPA, PDB 등을 환경별로 복사-붙여넣기하는 것은 유지보수 지옥이다. Helm은 이 문제를 템플릿 + 값 분리 패턴으로 해결한다.
Chart 구조 이해
my-app/
├── Chart.yaml # 차트 메타데이터 (이름, 버전, 의존성)
├── values.yaml # 기본 값 (오버라이드 가능)
├── values-dev.yaml # 개발 환경 값
├── values-prod.yaml # 운영 환경 값
├── templates/
│ ├── _helpers.tpl # 공통 템플릿 함수
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── serviceaccount.yaml
│ ├── pdb.yaml
│ └── tests/
│ └── test-connection.yaml
└── charts/ # 의존성 차트 (서브차트)
Chart.yaml 작성
apiVersion: v2
name: my-app
description: Backend API 서비스
type: application
version: 1.4.0 # Chart 버전 (패키징 버전)
appVersion: "2.1.0" # 애플리케이션 버전 (Docker 이미지 태그)
# 의존성 차트
dependencies:
- name: postgresql
version: "13.2.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled # values에서 on/off 제어
- name: redis
version: "18.4.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
values.yaml 설계
values.yaml은 Chart의 인터페이스다. 잘 설계하면 사용자가 templates를 읽지 않고도 배포를 커스터마이징할 수 있다.
# values.yaml — 기본값 + 주석으로 문서화
replicaCount: 2
image:
repository: ghcr.io/myorg/my-app
tag: "" # 비워두면 Chart.appVersion 사용
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.example.com
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 70
env:
DATABASE_URL: ""
REDIS_URL: ""
LOG_LEVEL: "info"
secrets:
DATABASE_PASSWORD: ""
JWT_SECRET: ""
# 의존성 제어
postgresql:
enabled: true
auth:
database: myapp
redis:
enabled: false
템플릿 작성: Go Template 핵심
# templates/_helpers.tpl — 재사용 가능한 템플릿 함수
{{/*
차트 이름
*/}}
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
풀네임: release-name + chart-name
*/}}
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
공통 라벨
*/}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
셀렉터 라벨
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
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:
# ConfigMap/Secret 변경 시 자동 롤링 업데이트
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "my-app.fullname" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
envFrom:
- configMapRef:
name: {{ include "my-app.fullname" . }}
- secretRef:
name: {{ include "my-app.fullname" . }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
조건부 리소스 생성
# templates/hpa.yaml — autoscaling.enabled가 true일 때만 생성
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "my-app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilization }}
{{- end }}
# templates/pdb.yaml — 최소 가용성 보장
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "my-app.fullname" . }}
spec:
minAvailable: 1
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
환경별 배포: values 오버라이드
# values-dev.yaml — 개발 환경
replicaCount: 1
image:
tag: "dev-latest"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: false
ingress:
hosts:
- host: api-dev.example.com
paths:
- path: /
pathType: Prefix
env:
LOG_LEVEL: "debug"
postgresql:
enabled: true
auth:
database: myapp_dev
# 배포 명령어
# 개발 환경
helm upgrade --install my-app ./my-app
-f values-dev.yaml
-n dev --create-namespace
# 운영 환경
helm upgrade --install my-app ./my-app
-f values-prod.yaml
--set secrets.DATABASE_PASSWORD=$DB_PASS
--set secrets.JWT_SECRET=$JWT_SECRET
-n production
# 디버깅: 렌더링 결과만 확인 (배포 안 함)
helm template my-app ./my-app -f values-dev.yaml
# Dry-run: 서버 검증까지 포함
helm upgrade --install my-app ./my-app
-f values-dev.yaml --dry-run --debug
Helm Hooks: 배포 전후 작업
DB 마이그레이션, 시드 데이터 삽입, 슬랙 알림 등을 배포 라이프사이클에 연동한다.
# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5" # 낮을수록 먼저 실행
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["npm", "run", "migration:run"]
envFrom:
- secretRef:
name: {{ include "my-app.fullname" . }}
릴리스 관리 명령어
| 명령어 | 설명 |
|---|---|
helm list -n prod |
네임스페이스의 릴리스 목록 |
helm history my-app -n prod |
릴리스 히스토리 (리비전 목록) |
helm rollback my-app 3 -n prod |
리비전 3으로 롤백 |
helm get values my-app -n prod |
현재 적용된 values 확인 |
helm get manifest my-app -n prod |
렌더링된 매니페스트 확인 |
helm diff upgrade my-app ./my-app |
변경될 내용 diff (플러그인) |
helm uninstall my-app -n prod |
릴리스 삭제 |
CI/CD 통합: GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & Push Image
run: |
docker build -t ghcr.io/myorg/my-app:${{ github.sha }} .
docker push ghcr.io/myorg/my-app:${{ github.sha }}
- uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Helm Upgrade
run: |
helm upgrade --install my-app ./helm/my-app
-f ./helm/my-app/values-prod.yaml
--set image.tag=${{ github.sha }}
--set secrets.DATABASE_PASSWORD=${{ secrets.DB_PASS }}
-n production
--wait --timeout 5m
K8s StatefulSet 가이드에서 다룬 상태 저장 워크로드도 Helm Chart로 패키징하면 환경별 PVC 크기, 레플리카 수를 values로 간편하게 제어할 수 있다.
Chart 테스트
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ include "my-app.fullname" . }}-test
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: test
image: busybox
command: ['wget', '--spider', '-q',
'http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/health']
# 실행: helm test my-app -n dev
# Lint 검사
helm lint ./my-app -f values-dev.yaml
# JSON Schema 검증 (values 타입 안전성)
# values.schema.json으로 입력값 검증 가능
Helm vs Kustomize 선택 기준
| 기준 | Helm | Kustomize |
|---|---|---|
| 접근 방식 | 템플릿 엔진 | 패치/오버레이 |
| 학습 곡선 | Go Template 필요 | YAML만 알면 됨 |
| 패키지 배포 | Chart Repository, OCI | Git 기반 |
| 롤백 | 내장 (helm rollback) | GitOps 도구 필요 |
| 추천 상황 | 복잡한 앱, 외부 배포 | 단순 환경 분기 |
Kustomize 환경별 배포 가이드와 비교하여 프로젝트에 맞는 도구를 선택하면 된다. 복잡한 마이크로서비스에서는 Helm + ArgoCD 조합이 가장 널리 쓰인다.
정리: Helm 운영 체크리스트
- values.yaml 문서화: 주석으로 각 값의 의미와 기본값 설명
- _helpers.tpl 활용: 이름, 라벨 등 반복 로직을 함수로 추출
- 환경별 values 분리: values-dev.yaml, values-prod.yaml로 관리
- checksum 어노테이션: ConfigMap/Secret 변경 시 자동 롤링 업데이트
- helm diff 필수: 배포 전 변경 내용 확인으로 사고 방지
- Hook으로 마이그레이션: pre-upgrade Hook에서 DB 마이그레이션 자동 실행
- Secrets는 –set으로: values 파일에 시크릿을 커밋하지 않는다