← 목록으로 돌아가기

Helm 차트 설계 실전: values 계층화·템플릿 함수·Umbrella 패턴으로 재사용 가능한 패키지 만들기

DevOps

Helm chart values templating best practices

Kubernetes에 배포할 때 YAML 파일을 그대로 복사해 환경마다 고치고 있다면, 이미 문제가 시작됐다

우리 팀이 처음 Helm을 도입한 프로젝트는 결제 플랫폼 마이크로서비스 12개를 Kubernetes 위에서 운영하는 환경이었습니다. 초기에는 각 서비스마다 deployment.yaml, service.yaml, configmap.yaml을 직접 작성했고, 환경이 dev·staging·production으로 나뉘면서 관리해야 할 YAML 파일이 36개를 넘어섰습니다. image tag 하나 바꾸려면 서비스마다 파일을 열어 수동으로 수정했고, 어느 날 staging에만 반영하고 production을 빠뜨린 탓에 30분 동안 알람이 울렸습니다.

Helm은 그 문제를 해결하는 도구입니다. 하지만 Helm을 단순히 "YAML 생성기"로 쓰면 또 다른 복잡도를 낳습니다. 템플릿 함수를 잘못 쓰면 렌더링 에러가 오직 helm install 시점에만 드러나고, values 계층이 뒤섞이면 어떤 값이 실제로 적용되는지 추적하기 어려워집니다. 이 글에서는 Chart.yaml 구성부터 values 우선순위 전략, Named Template 작성법, Sprig 함수 패턴, Umbrella Chart, OCI 레지스트리 배포, ArgoCD 통합까지 실무에서 반복적으로 확인된 패턴을 코드와 함께 정리합니다.


1. Helm은 패키지 매니저가 아니라 템플릿 엔진이다

Helm을 처음 소개할 때 "Kubernetes의 apt/yum"이라고 부르는 경우가 많습니다. 패키지 설치·업그레이드·제거라는 인터페이스가 apt와 닮아있기 때문입니다. 하지만 이 비유는 Helm의 실제 동작 방식을 숨깁니다. apt는 바이너리 패키지를 배포하고, 패키지 내용은 변경 없이 그대로 설치됩니다. Helm은 Go 템플릿 엔진 위에서 동작하고, 배포 시점에 values를 받아 YAML을 생성한 뒤 Kubernetes API에 전달합니다. 결과물인 YAML은 설치 환경과 전달된 values에 따라 매번 달라집니다.

Helm 공식 문서는 이 동작을 명확히 설명합니다. "Helm templates use the Go text/template package, extended by Sprig template functions and some Helm-specific functions." Helm 차트는 실행 가능한 프로그램에 가깝습니다. templates/ 디렉터리 안의 파일들은 함수를 가지고 있고, values와 Chart 메타데이터를 인수로 받아 렌더링 결과를 반환합니다.

이 관점 전환이 중요한 이유는 두 가지입니다. 첫째, 차트를 설계할 때 "어떤 YAML을 만들 것인가"가 아니라 "어떤 인터페이스(values)를 외부에 노출할 것인가"를 먼저 결정해야 합니다. 좋은 차트는 소비자가 알아야 할 최소한의 값만 values로 노출하고 나머지는 합리적인 기본값으로 채웁니다. 둘째, 템플릿 렌더링은 Kubernetes가 아닌 Helm 클라이언트에서 일어납니다. helm template 명령으로 클러스터 없이 렌더링 결과를 확인할 수 있고, 이것이 로컬 테스트의 기반이 됩니다.


2. Chart.yaml과 의존성 관리: SemVer가 깨지는 순간

Chart.yaml은 차트의 신원증명서입니다. name, version, appVersion, type, dependencies 필드가 핵심입니다. version은 차트 패키지 자체의 버전이고, appVersion은 차트가 배포하는 애플리케이션 버전입니다. 이 두 값은 독립적으로 움직입니다.

Helm 차트 구조 문서에 따르면, version 필드는 SemVer 2 규격을 따라야 합니다. 문제는 dependencies 필드에서 버전 범위를 잘못 지정할 때 발생합니다.

apiVersion: v2
name: payment-service
description: Payment microservice Helm chart
type: application
version: 1.4.0
appVersion: "2.3.1"

dependencies:
  - name: postgresql
    version: "15.5.x"          # patch 버전만 허용 — 권장
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
  - name: redis
    version: ">=19.0.0 <20.0.0" # minor 범위 허용 — 주의 필요
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
  - name: monitoring
    version: "~2.1.0"           # tilde: patch 허용
    repository: "oci://registry.example.com/charts"

>=19.0.0 <20.0.0처럼 범위를 넓게 잡으면 helm dependency update 실행 시점마다 다른 버전이 가져와질 수 있습니다. CI 파이프라인에서 빌드할 때마다 의존성이 달라지면 재현 불가능한 배포가 됩니다. Chart.lock 파일이 이 문제를 완화합니다. helm dependency update를 실행하면 실제로 사용한 버전이 Chart.lock에 고정되고, 이후 helm dependency build는 lock 파일 기준으로 의존성을 복원합니다. Chart.lock을 Git에 커밋해야 재현 가능한 빌드를 보장할 수 있습니다.


3. values 오버라이드 우선순위와 환경별 분리 전략

Helm의 values 오버라이드 우선순위는 다음 순서를 따릅니다. 낮은 번호가 낮은 우선순위입니다.

우선순위출처
1 (최저)차트 내 values.yaml
2부모 차트의 values.yaml
3-f 플래그로 전달한 파일
4--set 플래그 (문자열)
5--set-string 플래그
6 (최고)--set-file 플래그

환경별 values 파일 구성에서 흔히 보이는 실수는 환경마다 모든 값을 복사하는 것입니다. 권장하는 패턴은 기본 values.yaml에 dev 기준의 완전한 설정을 두고, 각 환경 파일에는 오버라이드 값만 담는 것입니다.

# values.yaml (기본값 — dev 기준)
replicaCount: 1
image:
  repository: registry.example.com/payment-service
  pullPolicy: IfNotPresent
  tag: "latest"
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 3

# values-production.yaml (prod 오버라이드만)
replicaCount: 3
image:
  pullPolicy: Always
  tag: ""  # CI에서 --set image.tag=<SHA>로 주입
resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "2000m"
    memory: "2Gi"
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20

배포 시에는 helm upgrade --install payment ./payment-service -f values-production.yaml --set image.tag=${GIT_SHA}처럼 기본값 위에 환경 파일을 쌓고, 빌드 시점 값만 --set으로 주입합니다. Kubernetes 오토스케일링 전략을 깊게 다룬 내용은 Kubernetes HPA·VPA·KEDA 운영 가이드에서 이어집니다.


4. Named Template과 _helpers.tpl: include vs template의 차이

Helm 차트에서 코드 중복을 없애는 핵심 도구가 Named Template입니다. _helpers.tpl 파일은 밑줄로 시작해 Kubernetes 리소스로 렌더링되지 않고, 재사용 가능한 템플릿 조각만 담습니다.

includetemplate의 차이는 파이프라인 연결 가능 여부입니다. template은 결과를 직접 출력하고 파이프로 연결할 수 없습니다. include는 결과를 문자열로 반환하므로 nindent, indent, trim 같은 함수와 파이프로 이을 수 있습니다. 실무에서는 항상 include를 사용해야 들여쓰기를 제어할 수 있습니다.

# templates/_helpers.tpl

{{/* fullname  63 제한, DNS RFC 1123 준수 */}}
{{- define "payment-service.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 "payment-service.labels" -}}
helm.sh/chart: {{ include "payment-service.chart" . }}
{{ include "payment-service.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/* 셀렉터 레이블 */}}
{{- define "payment-service.selectorLabels" -}}
app.kubernetes.io/name: {{ include "payment-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

nindent 4는 "앞에 개행 문자를 붙인 뒤 4칸 들여쓰기"를 의미합니다. indent 4와 달리 첫 줄 앞에도 개행을 추가하므로 멀티라인 블록을 YAML에 삽입할 때 더 안전합니다.


5. Sprig 함수 실전 패턴: tpl, default, required, ternary

Helm 템플릿에서 사용하는 함수는 Go 내장 함수와 Sprig 라이브러리의 조합입니다. Sprig는 문자열 조작, 날짜, 수학, 암호화, 딕셔너리 처리에 필요한 함수를 제공합니다.

default 는 값이 비어있을 때 대체값을 반환합니다. Go 템플릿에서 "비어있다"는 빈 문자열 "", 숫자 0, false, 빈 슬라이스, nil을 모두 포함합니다.

required 는 값이 없으면 렌더링을 실패시키고 에러 메시지를 출력합니다. 선택적 값에는 default를, 반드시 제공되어야 하는 값에는 required를 씁니다.

ternary 는 조건에 따라 두 값 중 하나를 선택합니다. 중첩하면 가독성이 급격히 떨어지므로 Named Template 안에서만 쓰거나 단순한 조건에만 적용합니다.

tpl 은 values에 담긴 문자열을 Helm 템플릿으로 평가합니다. 환경 변수나 ConfigMap 값 안에서 .Release.Name이나 .Values.global.domain 같은 템플릿 표현식을 쓰고 싶을 때 필요합니다.

# values.yaml
config:
  database:
    host: "postgresql.{{ .Release.Namespace }}.svc.cluster.local"
  callback:
    url: "https://{{ .Values.ingress.host }}/api/v1/callback"

# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "payment-service.fullname" . }}-config
data:
  DATABASE_HOST: {{ tpl .Values.config.database.host . | quote }}
  CALLBACK_URL: {{ tpl .Values.config.callback.url . | quote }}
  SECRET_NAME: {{ required "config.secretName은 필수입니다" .Values.config.secretName | quote }}
  LOG_LEVEL: {{ ternary "debug" "info" .Values.debug | quote }}

6. Umbrella Chart로 마이크로서비스 묶기

Umbrella Chart는 여러 마이크로서비스 차트를 하나의 상위 차트로 묶는 패턴입니다. 단일 helm install 명령으로 전체 스택을 배포하거나, 공통 values를 한 곳에서 관리할 수 있습니다.

Umbrella Chart의 주요 트레이드오프는 다음과 같습니다. 하위 차트를 독립적으로 업그레이드하기 어렵습니다. payment-service 하나만 업그레이드하려면 Umbrella 전체를 다시 배포하게 됩니다. 이를 해결하려면 ArgoCD의 App of Apps 패턴처럼 각 차트를 별도 Application으로 관리하면서 공통 values만 상위에서 공급하는 방식을 택합니다.


7. helm lint와 helm-unittest로 회귀 방지

Helm 차트는 배포 전 두 단계의 품질 검사를 거쳐야 합니다. helm lint는 구조적 오류를 잡고, helm-unittest는 렌더링 결과의 내용을 단언합니다.

# tests/deployment_test.yaml
suite: Deployment 렌더링 테스트
templates:
  - templates/deployment.yaml
  - templates/_helpers.tpl

tests:
  - it: 기본 values로 렌더링  replicas가 1이어야 한다
    asserts:
      - isKind:
          of: Deployment
      - equal:
          path: spec.replicas
          value: 1

  - it: autoscaling이 활성화되면 replicas 필드가 없어야 한다
    set:
      autoscaling.enabled: true
    asserts:
      - notExists:
          path: spec.replicas

컨테이너 이미지 보안 강화를 병행하려면 컨테이너 이미지 하드닝과 SBOM 관리를 함께 참고하면 좋습니다.


8. OCI 레지스트리에 차트 배포하기

Helm 3.8부터 OCI(Open Container Initiative) 레지스트리를 차트 저장소로 사용하는 기능이 GA 상태입니다. Helm OCI 공식 문서에 따르면 helm push, helm pull, helm install이 모두 oci:// 프로토콜을 지원합니다.

# 1. 차트 패키징
helm package ./payment-service --destination ./dist

# 2. OCI 레지스트리 로그인
helm registry login registry.example.com \
  --username "${REGISTRY_USERNAME}" \
  --password "${REGISTRY_PASSWORD}"

# 3. 차트 푸시
helm push ./dist/payment-service-1.4.0.tgz \
  oci://registry.example.com/charts

# 4. OCI 레지스트리에서 직접 설치
helm upgrade --install payment-service \
  oci://registry.example.com/charts/payment-service \
  --version 1.4.0 \
  -f values-production.yaml \
  --set image.tag="${GIT_SHA}" \
  --namespace payment \
  --create-namespace \
  --wait

AWS ECR의 경우 aws ecr get-login-password로 토큰을 받아 helm registry login에 전달해야 하고, 토큰 유효 시간이 12시간이므로 장기 실행 파이프라인에서는 갱신 로직이 필요합니다.


9. ArgoCD와 통합 시 chart values 관리 전략

ArgoCD는 Git 저장소를 단일 진실 공급원(Single Source of Truth)으로 삼아 Kubernetes 상태를 동기화합니다. Helm 차트와 ArgoCD를 통합할 때 values 관리 방식에 대한 결정이 필요합니다.

권장 패턴은 차트 코드와 환경 values를 별도 Git 저장소로 분리하는 것입니다. 차트 저장소(helm-charts)에는 재사용 가능한 차트 패키지를, 배포 설정 저장소(k8s-manifests)에는 ArgoCD Application 정의와 환경별 values 파일을 둡니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payment-service-production
  namespace: argocd
spec:
  project: production
  sources:
    - repoURL: https://github.com/example/k8s-manifests.git
      targetRevision: main
      ref: values
    - repoURL: registry.example.com/charts
      chart: payment-service
      targetRevision: "1.4.0"
      helm:
        valueFiles:
          - "$values/envs/production/payment-service.yaml"
  destination:
    server: https://kubernetes.default.svc
    namespace: payment-production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true

ArgoCD 2.6 이상에서 지원하는 multi-source 기능을 사용하면 차트는 OCI 레지스트리에서, values 파일은 Git 저장소에서 따로 가져올 수 있습니다.


10. 운영 체크리스트와 안티패턴

안티패턴 1: --set으로 모든 값을 주입하기. --set 플래그는 단순한 스칼라 값 오버라이드에 적합합니다. 중첩된 오브젝트나 배열을 --set으로 쓰면 경로 표현이 복잡해지고, 특수문자 이스케이프 문제가 생깁니다. --set-json이나 values 파일을 사용합니다.

안티패턴 2: 차트 내부에 시크릿 값을 하드코딩하기. values.yaml에 비밀번호나 API 키를 직접 쓰면 Git 히스토리에 노출됩니다. Kubernetes Secret을 existingSecret 패턴으로 참조하거나, External Secrets Operator, Vault Agent Injector를 사용합니다.

안티패턴 3: helm upgradehelm diff를 생략하기. helm-diff 플러그인을 설치하면 helm diff upgrade로 실제 적용될 변경 사항을 미리 볼 수 있습니다.


결론

Helm 차트의 품질은 설치 성공 여부가 아니라 6개월 뒤에도 팀원 누구나 이해하고 수정할 수 있느냐로 판가름됩니다.

  • Chart.yaml 의존성 버전은 patch까지 고정하고 Chart.lock을 Git에 커밋하고 있는가. 범위 지정은 재현 불가능한 빌드의 출발점입니다.
  • values.yaml에는 dev 기준의 완전한 기본값이 있고, 환경 파일에는 오버라이드만 담겨 있는가. 환경 파일 간 중복이 많으면 values 계층 설계를 재검토합니다.
  • _helpers.tpl의 Named Template을 모든 리소스에서 일관되게 사용하고, include로만 호출하고 있는가.
  • helm-unittest로 주요 values 조합의 렌더링 결과를 검증하는 테스트가 CI에 포함되어 있는가.
  • ArgoCD Application에서 차트 버전과 values 변경 이력이 Git 커밋으로 추적 가능한가.

Helm은 Kubernetes 리소스 관리의 복잡도를 낮춰주지만, 차트 자체의 복잡도를 관리하는 책임은 여전히 운영자에게 있습니다.