Skip to content
Published on

Helm Chart 작성과 배포 자동화 완벽 가이드

Authors
Helm Chart Authoring Guide

1. Helm이란?

Helm은 Kubernetes의 패키지 매니저입니다. 복잡한 Kubernetes 리소스를 하나의 Chart로 패키징하여 설치, 업그레이드, 롤백을 간편하게 관리할 수 있습니다.

왜 Helm인가?

  • 반복적인 YAML 작성 제거
  • 환경별 설정 분리 (dev/staging/production)
  • 버전 관리와 롤백
  • 의존성 관리

2. Chart 디렉토리 구조

mychart/
├── Chart.yaml          # Chart 메타데이터
├── Chart.lock          # 의존성 잠금 파일
├── values.yaml         # 기본 설정값
├── values-prod.yaml    # 프로덕션 오버라이드
├── templates/          # Kubernetes 매니페스트 템플릿
│   ├── _helpers.tpl    # 공통 헬퍼 함수
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── serviceaccount.yaml
│   ├── NOTES.txt       # 설치 후 안내 메시지
│   └── tests/
│       └── test-connection.yaml
├── charts/             # 의존성 Chart
└── .helmignore         # 패키징 제외 파일

Chart.yaml 작성

apiVersion: v2
name: my-web-app
description: A production-ready web application
type: application
version: 1.2.0 # Chart 버전 (SemVer)
appVersion: '3.1.0' # 앱 버전
maintainers:
  - name: youngjukim
    email: fjvbn2003@gmail.com
dependencies:
  - name: postgresql
    version: '12.x.x'
    repository: 'https://charts.bitnami.com/bitnami'
    condition: postgresql.enabled
  - name: redis
    version: '17.x.x'
    repository: 'https://charts.bitnami.com/bitnami'
    condition: redis.enabled

3. values.yaml 설계 패턴

# values.yaml - 기본 설정
replicaCount: 2

image:
  repository: myregistry.io/my-web-app
  tag: '' # Chart appVersion 사용
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: app.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: app-tls
      hosts:
        - app.example.com

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

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

postgresql:
  enabled: true
  auth:
    database: myapp
    username: appuser

redis:
  enabled: false

4. Templates 작성

_helpers.tpl - 공통 헬퍼

{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "mychart.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 }}

{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            initialDelaySeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          envFrom:
            - configMapRef:
                name: {{ include "mychart.fullname" . }}-config

5. Helm Hooks

Hook을 사용하면 릴리스 라이프사이클의 특정 시점에 작업을 실행할 수 있습니다:

# templates/pre-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-db-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          command: ["python", "manage.py", "migrate"]
          envFrom:
            - secretRef:
                name: {{ include "mychart.fullname" . }}-db-secret
  backoffLimit: 3

Hook 종류

Hook실행 시점
pre-install설치 전
post-install설치 후
pre-upgrade업그레이드 전
post-upgrade업그레이드 후
pre-rollback롤백 전
pre-delete삭제 전
testhelm test 실행 시

6. CI/CD 파이프라인 연동

GitHub Actions + Helm

# .github/workflows/deploy.yml
name: Deploy Helm Chart
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-2

      - name: Update kubeconfig
        run: aws eks update-kubeconfig --name my-cluster

      - name: Install Helm
        uses: azure/setup-helm@v3
        with:
          version: v3.14.0

      - name: Helm lint
        run: helm lint ./charts/my-web-app

      - name: Helm upgrade
        run: |
          helm upgrade --install my-app ./charts/my-web-app \
            --namespace production \
            --create-namespace \
            -f ./charts/my-web-app/values-prod.yaml \
            --set image.tag=${{ github.sha }} \
            --wait --timeout 5m

helm template로 로컬 검증

# 렌더링된 매니페스트 확인
helm template my-release ./mychart -f values-prod.yaml

# 특정 값 오버라이드
helm template my-release ./mychart --set replicaCount=3

# kubeval로 유효성 검증
helm template my-release ./mychart | kubeval --strict

7. Chart 테스트

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-test"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: curl
      image: curlimages/curl:8.5.0
      command: ['curl']
      args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}/healthz']
  restartPolicy: Never
# 테스트 실행
helm test my-release -n production

8. 퀴즈

Q1: Chart.yaml에서 version과 appVersion의 차이는?
  • version: Chart 자체의 버전입니다. Chart 구조, 템플릿, values가 변경될 때 올립니다. SemVer를 따릅니다.
  • appVersion: Chart가 배포하는 애플리케이션의 버전입니다. 앱 코드가 변경될 때 올립니다.

예: Chart version 1.2.0이 appVersion 3.1.0을 배포할 수 있습니다.

Q2: Helm Hook의 hook-weight는 어떤 역할을 하나요?

hook-weight는 같은 타입의 hook이 여러 개 있을 때 실행 순서를 결정합니다. 낮은 숫자가 먼저 실행됩니다.

예: weight -5인 DB 마이그레이션이 weight 0인 캐시 초기화보다 먼저 실행됩니다.

Q3: deployment.yaml에서 checksum/config 어노테이션의 용도는?

ConfigMap이 변경되면 sha256sum이 바뀌어 Pod의 annotation이 달라집니다. 이로 인해 Deployment가 rolling update를 트리거합니다.

ConfigMap만 변경하면 기본적으로 Pod가 재시작되지 않는 문제를 해결하는 패턴입니다.