Split View: Helm 템플릿 엔진 심층 분석: Go 템플릿, Sprig, 네임드 템플릿
Helm 템플릿 엔진 심층 분석: Go 템플릿, Sprig, 네임드 템플릿
- 1. Go 템플릿 내부 동작
- 2. Sprig 라이브러리 함수
- 3. 네임드 템플릿(Named Templates)
- 4. 플로우 컨트롤
- 5. 고급 템플릿 패턴
- 6. 라이브러리 차트(Library Charts)
- 7. 디버깅
- 8. 정리
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
template과 include의 핵심 차이:
# 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 템플릿 엔진은 단순한 문자열 치환을 넘어 강력한 프로그래밍 모델을 제공합니다:
- Go 템플릿 기반: 파싱-실행 2단계 파이프라인 아키텍처
- Sprig 라이브러리: 100개 이상의 유틸리티 함수 제공
- 네임드 템플릿: define/include로 재사용 가능한 템플릿 모듈화
- 플로우 컨트롤: if/with/range로 조건부 및 반복 렌더링
- 라이브러리 차트: 공통 로직의 중앙 관리와 공유
다음 글에서는 Helm 릴리스의 전체 생명주기(install, upgrade, rollback)를 분석합니다.
Helm Template Engine Deep Dive: Go Templates, Sprig, Named Templates
- 1. Go Template Internals
- 2. Sprig Library Functions
- 3. Named Templates
- 4. Flow Control
- 5. Advanced Template Patterns
- 6. Library Charts
- 7. Debugging
- 8. Summary
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:
- Go template-based: Two-stage parsing-execution pipeline architecture
- Sprig library: Over 100 utility functions
- Named templates: Modular, reusable templates with define/include
- Flow control: Conditional and iterative rendering with if/with/range
- Library charts: Centralized management and sharing of common logic
The next post analyzes the complete Helm release lifecycle (install, upgrade, rollback).