K8s Helm 템플릿 심화

Helm이란?

Helm은 Kubernetes의 패키지 매니저입니다. YAML 매니페스트를 직접 관리하는 대신, Chart라는 템플릿 패키지로 애플리케이션을 정의하고 배포합니다. 하지만 기본적인 helm install만으로는 실전 운영이 어렵습니다. 이 글에서는 Helm 템플릿의 Go template 문법, 헬퍼 함수, 조건부 렌더링, 서브차트 의존성까지 심화 패턴을 다룹니다.

Chart 디렉터리 구조

Helm Chart의 표준 구조를 먼저 이해해야 합니다.

my-app/
├── Chart.yaml          # 차트 메타데이터 (이름, 버전, 의존성)
├── values.yaml         # 기본 설정값
├── values-prod.yaml    # 환경별 오버라이드
├── templates/
│   ├── _helpers.tpl    # 재사용 헬퍼 함수
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── serviceaccount.yaml
│   ├── NOTES.txt       # 설치 후 안내 메시지
│   └── tests/
│       └── test-connection.yaml
└── charts/             # 서브차트 (의존성)

Go Template 핵심 문법

Helm 템플릿은 Go의 text/template 엔진을 기반으로 합니다. 핵심 문법을 정리합니다.

# 1. 값 참조 — .Values, .Release, .Chart
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
spec:
  replicas: {{ .Values.replicaCount }}

# 2. 파이프라인 — 값을 함수에 전달
# quote: 문자열을 따옴표로 감싸기
# upper/lower: 대소문자 변환
# default: 기본값 설정
  annotations:
    description: {{ .Values.description | default "No description" | quote }}
    env: {{ .Values.env | upper }}

# 3. 공백 제어 — {{- 와 -}} 로 앞뒤 공백 제거
metadata:
  labels:
    {{- range $key, $val := .Values.labels }}
    {{ $key }}: {{ $val | quote }}
    {{- end }}

조건부 렌더링과 반복

환경에 따라 리소스를 선택적으로 생성하거나, 동적 목록을 렌더링하는 패턴입니다. K8s ResourceQuota 같은 리소스도 조건부로 생성할 수 있습니다.

# if/else — 조건부 리소스 생성
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-app.fullname" . }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "my-app.fullname" $ }}
                port:
                  number: {{ .port | default 80 }}
          {{- end }}
    {{- end }}
{{- end }}

# with — 스코프 변경으로 반복 참조 줄이기
{{- with .Values.resources }}
resources:
  {{- toYaml . | nindent 2 }}
{{- end }}

# range + 인덱스
env:
  {{- range $i, $env := .Values.extraEnv }}
  - name: {{ $env.name }}
    value: {{ $env.value | quote }}
  {{- end }}

_helpers.tpl 헬퍼 함수 설계

재사용 가능한 템플릿 조각을 define으로 정의하고 include로 호출합니다. 이것이 Helm 차트의 DRY 원칙 핵심입니다.

# templates/_helpers.tpl

# 차트 이름 (63자 제한, DNS 호환)
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

# 풀네임: release-chart (릴리스 충돌 방지)
{{- 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: {{ include "my-app.chart" . }}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end }}

# 셀렉터 레이블 — Deployment/Service 매칭용
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

# ServiceAccount 이름
{{- define "my-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "my-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

# 이미지 전체 경로 (레지스트리 + 리포 + 태그)
{{- define "my-app.image" -}}
{{- $registry := .Values.image.registry | default "docker.io" -}}
{{- $repo := .Values.image.repository -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s/%s:%s" $registry $repo $tag -}}
{{- end }}

values.yaml 설계 패턴

values.yaml은 차트의 인터페이스입니다. 구조적으로 잘 설계해야 사용자가 쉽게 커스터마이징할 수 있습니다.

# values.yaml — 구조적 설계 예시
replicaCount: 2

image:
  registry: docker.io
  repository: myorg/my-app
  tag: ""          # 빈 문자열 → Chart.AppVersion 사용
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: app.example.com
      paths:
        - path: /
          pathType: Prefix
  tls: []

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

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilization: 70
  targetMemoryUtilization: 80

# 환경 변수 주입
env:
  NODE_ENV: production
  LOG_LEVEL: info

# 시크릿 참조
envFromSecrets: []
  # - secretName: db-credentials
  #   key: DATABASE_URL
  #   envName: DATABASE_URL

# 프로브 설정
probes:
  liveness:
    path: /health
    initialDelaySeconds: 30
    periodSeconds: 10
  readiness:
    path: /ready
    initialDelaySeconds: 5
    periodSeconds: 5

serviceAccount:
  create: true
  name: ""
  annotations: {}

환경별 values 오버라이드

기본 values.yaml 위에 환경별 파일을 겹쳐서 적용하는 것이 운영의 핵심입니다.

# values-prod.yaml — 프로덕션 오버라이드
replicaCount: 4

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: "2"
    memory: 2Gi

autoscaling:
  enabled: true
  minReplicas: 4
  maxReplicas: 20

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rate-limit: "100"
  hosts:
    - host: api.myservice.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-tls
      hosts:
        - api.myservice.com

# 배포 명령:
# helm upgrade --install my-app ./my-app 
#   -f values.yaml 
#   -f values-prod.yaml 
#   --namespace production 
#   --set image.tag=v1.5.2

Deployment 템플릿 완성본

위의 모든 패턴을 결합한 실전 Deployment 템플릿입니다. K8s Drain·PDB와 함께 사용하면 무중단 배포가 가능합니다.

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 }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      annotations:
        # ConfigMap 변경 시 자동 롤아웃 트리거
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "my-app.serviceAccountName" . }}
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: {{ include "my-app.image" . }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          # 환경 변수: values.env 맵에서 자동 생성
          env:
            {{- range $key, $val := .Values.env }}
            - name: {{ $key }}
              value: {{ $val | quote }}
            {{- end }}
            {{- range .Values.envFromSecrets }}
            - name: {{ .envName }}
              valueFrom:
                secretKeyRef:
                  name: {{ .secretName }}
                  key: {{ .key }}
            {{- end }}
          # 프로브
          livenessProbe:
            httpGet:
              path: {{ .Values.probes.liveness.path }}
              port: http
            initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
            periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
          readinessProbe:
            httpGet:
              path: {{ .Values.probes.readiness.path }}
              port: http
            initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
            periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}

서브차트와 의존성 관리

Chart.yaml의 dependencies로 Redis, PostgreSQL 같은 인프라 컴포넌트를 서브차트로 포함할 수 있습니다.

# Chart.yaml
apiVersion: v2
name: my-app
version: 1.0.0
appVersion: "2.3.1"
dependencies:
  - name: redis
    version: "18.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled    # values에서 on/off
  - name: postgresql
    version: "13.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

# values.yaml에서 서브차트 설정
redis:
  enabled: true
  architecture: standalone
  auth:
    password: "my-redis-pass"
  master:
    resources:
      requests:
        cpu: 100m
        memory: 128Mi

postgresql:
  enabled: false    # 외부 DB 사용 시 비활성화

# 의존성 설치
# helm dependency update ./my-app
# helm dependency build ./my-app

helm template로 디버깅

배포 전 렌더링 결과를 확인하는 습관은 필수입니다. 실수로 잘못된 YAML이 클러스터에 적용되는 것을 방지합니다.

# 전체 렌더링 확인
helm template my-release ./my-app -f values-prod.yaml

# 특정 템플릿만 확인
helm template my-release ./my-app -s templates/deployment.yaml

# 문법 검증
helm lint ./my-app -f values-prod.yaml

# dry-run으로 서버 사이드 검증 (클러스터 필요)
helm install my-release ./my-app --dry-run --debug

# 특정 값 오버라이드 테스트
helm template my-release ./my-app 
  --set replicaCount=5 
  --set image.tag=v2.0.0 
  --set ingress.enabled=true

실전 팁: ConfigMap 변경 자동 롤아웃

ConfigMap을 수정해도 Deployment가 자동으로 재시작되지 않는 것은 Kubernetes의 유명한 함정입니다. Helm 템플릿에서 sha256sum을 활용하면 이 문제를 해결할 수 있습니다.

# deployment.yaml의 pod template annotations
template:
  metadata:
    annotations:
      # ConfigMap 내용이 바뀌면 해시가 달라져서 롤아웃 트리거
      checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "my-app.fullname" . }}
data:
  {{- range $key, $val := .Values.env }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}
  {{- if .Values.configFiles }}
  {{- range $filename, $content := .Values.configFiles }}
  {{ $filename }}: |
    {{- $content | nindent 4 }}
  {{- end }}
  {{- end }}

Helm 테스트 작성

배포 후 자동 검증을 위한 테스트 Pod를 정의할 수 있습니다.

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "my-app.fullname" . }}-test"
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox:1.36
      command: ['wget']
      args:
        - '--spider'
        - '--timeout=5'
        - 'http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/health'

# 테스트 실행
# helm test my-release --namespace production

정리

Helm 템플릿은 단순한 YAML 생성기가 아니라, Kubernetes 배포의 추상화 레이어입니다. _helpers.tpl로 DRY 원칙을 지키고, 조건부 렌더링으로 환경별 차이를 흡수하며, 서브차트로 의존성을 관리합니다. helm templatehelm lint로 배포 전 검증을 습관화하고, sha256sum 트릭으로 ConfigMap 변경을 자동 감지하세요. 잘 설계된 Helm Chart는 팀 전체의 배포 생산성을 끌어올립니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux