Skip to content

Split View: Helm 템플릿 엔진 심층 분석: Go 템플릿, Sprig, 네임드 템플릿

|

Helm 템플릿 엔진 심층 분석: Go 템플릿, Sprig, 네임드 템플릿


1. Go 템플릿 내부 동작

1.1 파싱(Parsing) 단계

Helm의 템플릿 엔진은 Go 표준 라이브러리의 text/template을 기반으로 합니다. 템플릿 처리는 크게 파싱실행 두 단계로 나뉩니다.

파싱 단계에서는 템플릿 텍스트를 **AST(Abstract Syntax Tree)**로 변환합니다:

템플릿 텍스트 → 렉서(Lexer) → 토큰 스트림 → 파서(Parser)AST

렉서는 템플릿에서 다음 요소를 인식합니다:

  • 일반 텍스트 (그대로 출력)
  • 액션 구분자: 여는 구분자와 닫는 구분자
  • 파이프라인, 변수, 함수 호출

1.2 실행(Execution) 단계

실행 단계에서는 AST를 순회하면서 데이터 컨텍스트(dot .)를 적용합니다:

AST + 데이터 컨텍스트 → 렌더링된 텍스트 출력

파이프라인(Pipeline) 개념이 핵심입니다:

# 파이프라인: 왼쪽 출력이 오른쪽 입력으로 전달
name: { { .Values.name | lower | trunc 63 | trimSuffix "-" } }

# 위 파이프라인의 실행 순서:
# 1. .Values.name 평가 → "My-Application-Name"
# 2. lower 적용 → "my-application-name"
# 3. trunc 63 적용 → "my-application-name" (63자 이하이므로 그대로)
# 4. trimSuffix "-" 적용 → "my-application-name"

1.3 공백 제어(Whitespace Control)

# 하이픈(-)으로 공백 제거
metadata:
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
    #^ 왼쪽 공백/줄바꿈 제거

    {{ include "my-chart.labels" . | nindent 4 -}}
    #                                           ^ 오른쪽 공백/줄바꿈 제거

    {{- include "my-chart.labels" . | nindent 4 -}}
    #^ 양쪽 모두 제거

2. Sprig 라이브러리 함수

2.1 문자열 함수

# 기본 문자열 조작
upper: {{ "hello" | upper }}          # HELLO
lower: {{ "HELLO" | lower }}          # hello
title: {{ "hello world" | title }}    # Hello World
untitle: {{ "Hello World" | untitle }}# hello world
trim: {{ "  hello  " | trim }}        # hello
trimAll: {{ "**hello**" | trimAll "*" }}  # hello

# 부분 문자열
substr: {{ substr 0 5 "hello world" }}    # hello
trunc: {{ "hello" | trunc 3 }}            # hel
contains: {{ contains "lo" "hello" }}     # true
hasPrefix: {{ hasPrefix "he" "hello" }}   # true
hasSuffix: {{ hasSuffix "lo" "hello" }}   # true

# 치환
replace: {{ "hello world" | replace " " "-" }}  # hello-world
snakecase: {{ "HelloWorld" | snakecase }}        # hello_world
camelcase: {{ "hello_world" | camelcase }}       # HelloWorld
kebabcase: {{ "HelloWorld" | kebabcase }}        # hello-world

# 정규 표현식
regexMatch: {{ regexMatch "^[a-z]+$" "hello" }}          # true
regexReplaceAll: {{ regexReplaceAll "[0-9]+" "abc123" "X" }}  # abcX

# 랜덤 문자열
randAlphaNum: {{ randAlphaNum 10 }}    # 10자 랜덤 영숫자

2.2 수학 함수

# 기본 산술
add: { { add 1 2 } } # 3
sub: { { sub 10 3 } } # 7
mul: { { mul 2 3 } } # 6
div: { { div 10 3 } } # 3
mod: { { mod 10 3 } } # 1
max: { { max 1 2 3 } } # 3
min: { { min 1 2 3 } } # 1

# 반올림
ceil: { { ceil 1.1 } } # 2
floor: { { floor 1.9 } } # 1
round: { { round 1.5 0 } } # 2

2.3 날짜 함수

# 현재 시간
now: { { now } }
date: { { now | date "2006-01-02" } }
dateInZone: { { dateInZone "2006-01-02" (now) "UTC" } }
htmlDate: { { now | htmlDate } }

# 날짜 계산
dateModify: { { now | dateModify "+24h" | date "2006-01-02" } }
ago: { { ago (now) } }

2.4 암호화 함수

# 해싱
sha256sum: {{ "hello" | sha256sum }}
sha1sum: {{ "hello" | sha1sum }}

# Base64
b64enc: {{ "hello" | b64enc }}          # aGVsbG8=
b64dec: {{ "aGVsbG8=" | b64dec }}       # hello

# 비밀번호 생성
genPassword: {{ randAlphaNum 16 }}
derivePassword: {{ derivePassword 1 "long" "password" "user" "example.com" }}

# UUID
uuid: {{ uuidv4 }}

2.5 리스트 함수

# 리스트 생성과 조작
list: { { list "a" "b" "c" } }
first: { { first (list "a" "b" "c") } } # a
last: { { last (list "a" "b" "c") } } # c
rest: { { rest (list "a" "b" "c") } } # [b c]
initial: { { initial (list "a" "b" "c") } } # [a b]
append: { { append (list "a" "b") "c" } } # [a b c]
prepend: { { prepend (list "b" "c") "a" } } # [a b c]
concat: { { concat (list "a") (list "b") } } # [a b]
has: { { has "b" (list "a" "b" "c") } } # true
without: { { without (list "a" "b" "c") "b" } } # [a c]
uniq: { { uniq (list "a" "b" "a") } } # [a b]
sortAlpha: { { sortAlpha (list "c" "a" "b") } } # [a b c]

2.6 딕셔너리 함수

# 딕셔너리 생성과 조작
{{- $myDict := dict "key1" "value1" "key2" "value2" }}
get: {{ get $myDict "key1" }}              # value1
set: {{ set $myDict "key3" "value3" }}
hasKey: {{ hasKey $myDict "key1" }}         # true
keys: {{ keys $myDict }}                    # [key1 key2]
values: {{ values $myDict }}                # [value1 value2]
pluck: {{ pluck "key1" $myDict }}           # [value1]

# merge: 여러 딕셔너리 병합
{{- $merged := merge (dict "a" "1") (dict "b" "2") }}

3. 네임드 템플릿(Named Templates)

3.1 define과 template

# templates/_helpers.tpl
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

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

3.2 template vs include

templateinclude의 핵심 차이:

# template: 결과를 직접 출력 (파이프라인 사용 불가)
{{ template "my-chart.name" . }}

# include: 결과를 문자열로 반환 (파이프라인 사용 가능)
{{ include "my-chart.labels" . | nindent 4 }}

include를 사용하는 것이 권장됩니다. 파이프라인을 통해 후처리(들여쓰기 등)가 가능하기 때문입니다.

3.3 _helpers.tpl 관례

# templates/_helpers.tpl - 표준 헬퍼 패턴

# 차트 이름
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

# 전체 이름
{{- define "my-chart.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}

# 차트 버전 레이블
{{- define "my-chart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

# 공통 레이블
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

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

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

4. 플로우 컨트롤

4.1 if/else

# 기본 조건문
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-chart.fullname" . }}
spec:
  rules:
    - host: {{ .Values.ingress.host }}
{{- end }}

# if/else if/else
{{- if eq .Values.service.type "NodePort" }}
  nodePort: {{ .Values.service.nodePort }}
{{- else if eq .Values.service.type "LoadBalancer" }}
  loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- else }}
  # ClusterIP - no extra config
{{- end }}

Falsy 값: false, 0, 빈 문자열 "", nil, 빈 컬렉션(list, map, tuple, dict)

4.2 with

with는 스코프를 변경합니다:

{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

# 주의: with 블록 내에서 상위 스코프 접근 시 $를 사용
{{- with .Values.container }}
  name: {{ $.Chart.Name }}
  image: {{ .image }}
  port: {{ .port }}
{{- end }}

4.3 range

# 리스트 반복
{{- range .Values.extraEnvVars }}
- name: {{ .name }}
  value: {{ .value | quote }}
{{- end }}

# 맵 반복
{{- range $key, $value := .Values.configData }}
{{ $key }}: {{ $value | quote }}
{{- end }}

# 인덱스와 함께 반복
{{- range $index, $item := .Values.items }}
item-{{ $index }}: {{ $item }}
{{- end }}

# 고정 횟수 반복
{{- range until 5 }}
- replica-{{ . }}
{{- end }}

5. 고급 템플릿 패턴

5.1 toYaml과 nindent

# toYaml: Go 객체를 YAML 문자열로 변환
# nindent: n칸 들여쓰기 추가 (줄바꿈 포함)
spec:
  template:
    spec:
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}

5.2 tpl 함수

tpl은 문자열을 템플릿으로 렌더링합니다:

# values.yaml
configTemplate: |
  server.name={{ .Release.Name }}
  server.namespace={{ .Release.Namespace }}

# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: { { include "my-chart.fullname" . } }
data:
  config.properties: |
    {{ tpl .Values.configTemplate . | nindent 4 }}

5.3 lookup 함수

실행 시점에 클러스터 리소스를 조회합니다:

# 기존 Secret이 있으면 재사용, 없으면 생성
{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
password: {{ index $secret.data "password" }}
{{- else }}
password: {{ randAlphaNum 16 | b64enc }}
{{- end }}

주의: helm template 명령에서는 lookup이 항상 빈 결과를 반환합니다.

5.4 .Files 객체

# 파일 내용을 ConfigMap에 포함
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "my-chart.fullname" . }}-config
data:
  {{- range $path, $_ := .Files.Glob "config/**" }}
  {{ base $path }}: |
    {{ $.Files.Get $path | nindent 4 }}
  {{- end }}

# 바이너리 파일 (base64)
binaryData:
  {{- range $path, $_ := .Files.Glob "binaries/**" }}
  {{ base $path }}: {{ $.Files.Get $path | b64enc }}
  {{- end }}

6. 라이브러리 차트(Library Charts)

6.1 라이브러리 차트란

라이브러리 차트는 type: library로 선언된 차트로, 자체 리소스를 렌더링하지 않고 네임드 템플릿만 제공합니다.

# Chart.yaml
apiVersion: v2
name: my-library
type: library
version: 1.0.0

6.2 라이브러리 차트 활용

# 부모 차트의 Chart.yaml
dependencies:
  - name: my-library
    version: '1.x.x'
    repository: 'https://charts.example.com'

# 부모 차트의 템플릿에서 라이브러리 함수 사용
metadata:
  labels: { { - include "my-library.standardLabels" . | nindent 4 } }

라이브러리 차트의 장점:

  • 여러 차트에서 공통 템플릿 로직을 공유
  • 표준 레이블, 어노테이션, 리소스 패턴을 중앙에서 관리
  • DRY(Don't Repeat Yourself) 원칙 준수

7. 디버깅

7.1 helm template

# 로컬에서 템플릿 렌더링 (클러스터 불필요)
helm template my-release ./my-chart

# 특정 values 파일 적용
helm template my-release ./my-chart -f production-values.yaml

# 특정 템플릿만 렌더링
helm template my-release ./my-chart -s templates/deployment.yaml

# 디버그 출력
helm template my-release ./my-chart --debug

7.2 helm lint

# 차트 문법 검사
helm lint ./my-chart

# 엄격 모드 (경고도 오류로 처리)
helm lint ./my-chart --strict

# values 파일과 함께 검증
helm lint ./my-chart -f production-values.yaml

8. 정리

Helm 템플릿 엔진은 단순한 문자열 치환을 넘어 강력한 프로그래밍 모델을 제공합니다:

  1. Go 템플릿 기반: 파싱-실행 2단계 파이프라인 아키텍처
  2. Sprig 라이브러리: 100개 이상의 유틸리티 함수 제공
  3. 네임드 템플릿: define/include로 재사용 가능한 템플릿 모듈화
  4. 플로우 컨트롤: if/with/range로 조건부 및 반복 렌더링
  5. 라이브러리 차트: 공통 로직의 중앙 관리와 공유

다음 글에서는 Helm 릴리스의 전체 생명주기(install, upgrade, rollback)를 분석합니다.

Helm Template Engine Deep Dive: Go Templates, Sprig, Named Templates


1. Go Template Internals

1.1 Parsing Stage

Helm's template engine is built on Go's text/template standard library. Template processing is divided into parsing and execution stages.

The parsing stage transforms template text into an AST (Abstract Syntax Tree):

Template text -> Lexer -> Token stream -> Parser -> AST

1.2 Execution Stage

The execution stage traverses the AST while applying the data context (dot .):

AST + Data context -> Rendered text output

The Pipeline concept is key:

# Pipeline: left output is passed as right input
name: { { .Values.name | lower | trunc 63 | trimSuffix "-" } }

# Execution order:
# 1. Evaluate .Values.name -> "My-Application-Name"
# 2. Apply lower -> "my-application-name"
# 3. Apply trunc 63 -> "my-application-name"
# 4. Apply trimSuffix "-" -> "my-application-name"

1.3 Whitespace Control

# Hyphens (-) remove whitespace
metadata:
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
    #^ removes left whitespace/newlines

    {{ include "my-chart.labels" . | nindent 4 -}}
    #                                           ^ removes right whitespace/newlines

    {{- include "my-chart.labels" . | nindent 4 -}}
    #^ removes both sides

2. Sprig Library Functions

2.1 String Functions

upper: {{ "hello" | upper }}          # HELLO
lower: {{ "HELLO" | lower }}          # hello
title: {{ "hello world" | title }}    # Hello World
trim: {{ "  hello  " | trim }}        # hello
substr: {{ substr 0 5 "hello world" }}# hello
trunc: {{ "hello" | trunc 3 }}        # hel
contains: {{ contains "lo" "hello" }} # true
replace: {{ "hello world" | replace " " "-" }}  # hello-world
snakecase: {{ "HelloWorld" | snakecase }}        # hello_world
camelcase: {{ "hello_world" | camelcase }}       # HelloWorld
kebabcase: {{ "HelloWorld" | kebabcase }}        # hello-world
regexMatch: {{ regexMatch "^[a-z]+$" "hello" }} # true
randAlphaNum: {{ randAlphaNum 10 }}    # 10-char random alphanumeric

2.2 Math Functions

add: { { add 1 2 } } # 3
sub: { { sub 10 3 } } # 7
mul: { { mul 2 3 } } # 6
div: { { div 10 3 } } # 3
mod: { { mod 10 3 } } # 1
max: { { max 1 2 3 } } # 3
min: { { min 1 2 3 } } # 1
ceil: { { ceil 1.1 } } # 2
floor: { { floor 1.9 } } # 1
round: { { round 1.5 0 } } # 2

2.3 Date Functions

now: { { now } }
date: { { now | date "2006-01-02" } }
dateModify: { { now | dateModify "+24h" | date "2006-01-02" } }

2.4 Crypto Functions

sha256sum: {{ "hello" | sha256sum }}
b64enc: {{ "hello" | b64enc }}          # aGVsbG8=
b64dec: {{ "aGVsbG8=" | b64dec }}       # hello
uuid: {{ uuidv4 }}

2.5 List Functions

list: { { list "a" "b" "c" } }
first: { { first (list "a" "b" "c") } } # a
last: { { last (list "a" "b" "c") } } # c
append: { { append (list "a" "b") "c" } } # [a b c]
has: { { has "b" (list "a" "b" "c") } } # true
without: { { without (list "a" "b" "c") "b" } } # [a c]
uniq: { { uniq (list "a" "b" "a") } } # [a b]
sortAlpha: { { sortAlpha (list "c" "a" "b") } } # [a b c]

2.6 Dictionary Functions

{{- $myDict := dict "key1" "value1" "key2" "value2" }}
get: {{ get $myDict "key1" }}              # value1
hasKey: {{ hasKey $myDict "key1" }}         # true
keys: {{ keys $myDict }}                    # [key1 key2]
values: {{ values $myDict }}                # [value1 value2]
{{- $merged := merge (dict "a" "1") (dict "b" "2") }}

3. Named Templates

3.1 define and template

# templates/_helpers.tpl
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

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

3.2 template vs include

# template: outputs directly (no pipeline)
{{ template "my-chart.name" . }}

# include: returns as string (pipeline available)
{{ include "my-chart.labels" . | nindent 4 }}

include is recommended because it supports post-processing via pipelines (indentation, etc.).

3.3 _helpers.tpl Convention

# Standard helper patterns
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

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

4. Flow Control

4.1 if/else

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-chart.fullname" . }}
{{- end }}

{{- if eq .Values.service.type "NodePort" }}
  nodePort: {{ .Values.service.nodePort }}
{{- else if eq .Values.service.type "LoadBalancer" }}
  loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

Falsy values: false, 0, empty string "", nil, empty collections

4.2 with

{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

# Access parent scope with $ inside with blocks
{{- with .Values.container }}
  name: {{ $.Chart.Name }}
  image: {{ .image }}
{{- end }}

4.3 range

# List iteration
{{- range .Values.extraEnvVars }}
- name: {{ .name }}
  value: {{ .value | quote }}
{{- end }}

# Map iteration
{{- range $key, $value := .Values.configData }}
{{ $key }}: {{ $value | quote }}
{{- end }}

# Fixed count
{{- range until 5 }}
- replica-{{ . }}
{{- end }}

5. Advanced Template Patterns

5.1 toYaml and nindent

spec:
  template:
    spec:
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

5.2 tpl Function

# values.yaml
configTemplate: |
  server.name={{ .Release.Name }}

# templates/configmap.yaml
data:
  config.properties: |
    {{ tpl .Values.configTemplate . | nindent 4 }}

5.3 lookup Function

{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
password: {{ index $secret.data "password" }}
{{- else }}
password: {{ randAlphaNum 16 | b64enc }}
{{- end }}

Note: lookup always returns empty results with helm template.


6. Library Charts

Library charts (type: library) render no resources themselves, only provide named templates:

# Chart.yaml
apiVersion: v2
name: my-library
type: library
version: 1.0.0

Benefits: shared template logic, centralized label/annotation standards, DRY compliance.


7. Debugging

# Local template rendering
helm template my-release ./my-chart
helm template my-release ./my-chart -s templates/deployment.yaml
helm template my-release ./my-chart --debug

# Lint
helm lint ./my-chart --strict

8. Summary

The Helm template engine provides a powerful programming model beyond simple string substitution:

  1. Go template-based: Two-stage parsing-execution pipeline architecture
  2. Sprig library: Over 100 utility functions
  3. Named templates: Modular, reusable templates with define/include
  4. Flow control: Conditional and iterative rendering with if/with/range
  5. Library charts: Centralized management and sharing of common logic

The next post analyzes the complete Helm release lifecycle (install, upgrade, rollback).