Helm Chart 실전 운영 가이드

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 파일에 시크릿을 커밋하지 않는다
위로 스크롤
WordPress Appliance - Powered by TurnKey Linux